actix-web-4.9.0/.cargo_vcs_info.json0000644000000001470000000000100127330ustar { "git": { "sha1": "e0918fb1795c899ac38fb903694ca1aa3b58d46d" }, "path_in_vcs": "actix-web" }actix-web-4.9.0/CHANGES.md000064400000000000000000001541251046102023000131230ustar 00000000000000# Changelog ## Unreleased ## 4.9.0 ### Added - Add `middleware::from_fn()` helper. - Add `web::ThinData` extractor. ## 4.8.0 ### Added - Add `web::Html` responder. - Add `HttpRequest::full_url()` method to get the complete URL of the request. ### Fixed - Always remove port from return value of `ConnectionInfo::realip_remote_addr()` when handling IPv6 addresses. from the `Forwarded` header. - The `UrlencodedError::ContentType` variant (relevant to the `Form` extractor) now uses the 415 (Media Type Unsupported) status code in it's `ResponseError` implementation. - Apply `HttpServer::max_connection_rate()` setting when using rustls v0.22 or v0.23. ## 4.7.0 ### Added - Add `#[scope]` macro. - Add `middleware::Identity` type. - Add `CustomizeResponder::add_cookie()` method. - Add `guard::GuardContext::app_data()` method. - Add `compat-routing-macros-force-pub` crate feature which (on-by-default) which, when disabled, causes handlers to inherit their attached function's visibility. - Add `compat` crate feature group (on-by-default) which, when disabled, helps with transitioning to some planned v5.0 breaking changes, starting only with `compat-routing-macros-force-pub`. - Implement `From>` for `Error`. ## 4.6.0 ### Added - Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. - Add `rustls-0_23` crate feature. - Add `HttpServer::{bind_rustls_0_23, listen_rustls_0_23}()` builder methods. - Add `HttpServer::tls_handshake_timeout()` builder method for `rustls-0_22` and `rustls-0_23`. ### Changed - Update `brotli` dependency to `6`. - Minimum supported Rust version (MSRV) is now 1.72. ### Fixed - Avoid type confusion with `rustls` in some circumstances. ## 4.5.1 ### Fixed - Fix missing import when using enabling Rustls v0.22 support. ## 4.5.0 ### Added - Add `rustls-0_22` crate feature. - Add `HttpServer::{bind_rustls_0_22, listen_rustls_0_22}()` builder methods. ## 4.4.1 ### Changed - Updated `zstd` dependency to `0.13`. - Compression middleware now prefers brotli over zstd over gzip. ### Fixed - Fix validation of `Json` extractor when `JsonConfig::validate_content_type()` is set to false. ## 4.4.0 ### Added - Add `HttpServer::{bind, listen}_auto_h2c()` methods behind new `http2` crate feature. - Add `HttpServer::{bind, listen}_rustls_021()` methods for Rustls v0.21 support behind new `rustls-0_21` crate feature. - Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards. - Add `web::Payload::to_bytes[_limited]()` helper methods. - Add missing constructors on `HttpResponse` for several status codes. - Add `http::header::ContentLength` typed header. - Implement `Default` for `web::Data`. - Implement `serde::Deserialize` for `web::Data`. - Add `rustls-0_20` crate feature, which the existing `rustls` feature now aliases. ### Changed - Handler functions can now receive up to 16 extractor parameters. - The `Compress` middleware no longer compresses image or video content. - Hide sensitive header values in `HttpRequest`'s `Debug` output. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. ## 4.3.1 ### Added - Add support for custom methods with the `#[route]` macro. [#2969] [#2969]: https://github.com/actix/actix-web/pull/2969 ## 4.3.0 ### Added - Add `ContentDisposition::attachment()` constructor. [#2867] - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `Logger::custom_response_replace()`. [#2631] - Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] - Add `guard::Acceptable` for matching against `Accept` header MIME types. [#2265] - Add fallible versions of `test` helpers: `try_call_service()`, `try_call_and_read_body_json()`, `try_read_body()`, and `try_read_body_json()`. [#2961] ### Fixed - Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949] [#1961]: https://github.com/actix/actix-web/pull/1961 [#2265]: https://github.com/actix/actix-web/pull/2265 [#2631]: https://github.com/actix/actix-web/pull/2631 [#2784]: https://github.com/actix/actix-web/pull/2784 [#2867]: https://github.com/actix/actix-web/pull/2867 [#2949]: https://github.com/actix/actix-web/pull/2949 [#2961]: https://github.com/actix/actix-web/pull/2961 ## 4.2.1 ### Fixed - Bump minimum version of `actix-http` dependency to fix compatibility issue. [#2871] [#2871]: https://github.com/actix/actix-web/pull/2871 ## 4.2.0 ### Added - Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] ### Changed - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2718]: https://github.com/actix/actix-web/pull/2718 [#2752]: https://github.com/actix/actix-web/pull/2752 [#2786]: https://github.com/actix/actix-web/pull/2786 ## 4.1.0 ### Added - Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647] - Add `Route::wrap()` to allow individual routes to use middleware. [#2725] - Add `ServiceConfig::default_service()`. [#2338] [#2743] - Implement `ResponseError` for `std::convert::Infallible` ### Changed - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. ### Fixed - Clear connection-level data on `HttpRequest` drop. [#2742] [#2338]: https://github.com/actix/actix-web/pull/2338 [#2647]: https://github.com/actix/actix-web/pull/2647 [#2725]: https://github.com/actix/actix-web/pull/2725 [#2742]: https://github.com/actix/actix-web/pull/2742 [#2743]: https://github.com/actix/actix-web/pull/2743 ## 4.0.1 ### Fixed - Use stable version in readme example. ## 4.0.0 ### Dependencies - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `actix-web-codegen` to `4.0.0`. - Updated `cookie` to `0.16`. [#2555] - Updated `language-tags` to `0.3`. - Updated `rand` to `0.8`. - Updated `rustls` to `0.20`. [#2414] - Updated `tokio` to `1`. ### Added - Crate Features: - `cookies`; enabled by default. [#2619] - `compress-brotli`; enabled by default. [#2618] - `compress-gzip`; enabled by default. [#2618] - `compress-zstd`; enabled by default. [#2618] - `macros`; enables routing and runtime macros, enabled by default. [#2619] - Types: - `CustomizeResponder` for customizing response. [#2510] - `dev::ServerHandle` re-export from `actix-server`. [#2442] - `dev::ServiceFactory` re-export from `actix-service`. [#2325] - `guard::GuardContext` for use with the `Guard` trait. [#2552] - `http::header::AcceptEncoding` typed header. [#2482] - `http::header::Range` typed header. [#2485] - `http::KeepAlive` re-export from `actix-http`. [#2625] - `middleware::Compat` that boxes middleware types like `Logger` and `Compress` to be used with constrained type bounds. [#1865] - `web::Header` extractor for extracting typed HTTP headers in handlers. [#2094] - Methods: - `dev::ServiceRequest::guard_ctx()` for obtaining a guard context. [#2552] - `dev::ServiceRequest::parts_mut()`. [#2177] - `dev::ServiceResponse::map_into_{left,right}_body()` and `HttpResponse::map_into_boxed_body()`. [#2468] - `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] - `http::header::AcceptLanguage::{ranked, preference}()`. [#2480] - `HttpResponse::add_removal_cookie()`. [#2586] - `HttpResponse::map_into_{left,right}_body()` and `HttpResponse::map_into_boxed_body()`. [#2468] - `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] - `middleware::Logger::log_target()` to allow customize. [#2594] - `Responder::customize()` trait method that wraps responder in `CustomizeResponder`. [#2510] - `Route::service()` for using hand-written services as handlers. [#2262] - `ServiceResponse::into_parts()`. [#2499] - `TestServer::client_headers()` method. [#2097] - `web::ServiceConfig::configure()` to allow easy nesting of configuration functions. [#1988] - Trait Implementations: - Implement `Debug` for `DefaultHeaders`. [#2510] - Implement `FromRequest` for `ConnectionInfo` and `PeerAddr`. [#2263] - Implement `FromRequest` for `Method`. [#2263] - Implement `FromRequest` for `Uri`. [#2263] - Implement `Hash` for `http::header::Encoding`. [#2501] - Implement `Responder` for `Vec`. [#2625] - Misc: - `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] - Enable registering a vec of services of the same type to `App` [#1933] - Add `services!` macro for helping register multiple services to `App`. [#1933] - Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] - Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] ### Changed - Functions: - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - `guard::Not` is now generic over the type of guard it wraps. [#2552] - `test::{call_service, read_response, read_response_json, send_request}()` now receive a `&Service`. [#1905] - Some guard functions now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] - Rename `test::{default_service => status_service}()`. Old name is deprecated. [#2518] - Rename `test::{read_response_json => call_and_read_body_json}()`. Old name is deprecated. [#2518] - Rename `test::{read_response => call_and_read_body}()`. Old name is deprecated. [#2518] - Traits: - `guard::Guard::check` now receives a `&GuardContext`. [#2552] - `FromRequest::Config` associated type was removed. [#2233] - `Responder` trait has been reworked and now `Response`/`HttpResponse` synchronously, making it simpler and more performant. [#1891] - Rename `Factory` trait to `Handler`. [#1852] - Types: - `App`'s `B` (body) type parameter been removed. As a result, `App`s can be returned from functions now. [#2493] - `Compress` middleware's response type is now `EitherBody>`. [#2448] - `error::BlockingError` is now a unit struct. It's now only triggered when blocking thread pool has shutdown. [#1957] - `ErrorHandlerResponse`'s response variants now use `ServiceResponse>`. [#2515] - `ErrorHandlers` middleware's response types now use `ServiceResponse>`. [#2515] - `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] - `middleware::Condition` gained a broader middleware compatibility. [#2635] - `Resource` no longer require service body type to be boxed. [#2526] - `Scope` no longer require service body type to be boxed. [#2523] - `web::Path`s inner field is now private. [#1894] - `web::Payload`'s inner field is now private. [#2384] - Error enums are now marked `#[non_exhaustive]`. [#2148] - Enum Variants: - `Either` now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] - Include size and limits in `JsonPayloadError::Overflow`. [#2162] - Methods: - `App::data()` is deprecated; `App::app_data()` should be preferred. [#2271] - `dev::JsonBody::new()` returns a default limit of 32kB to be consistent with `JsonConfig` and the default behaviour of the `web::Json` extractor. [#2010] - `dev::ServiceRequest::{into_parts, from_parts}()` can no longer fail. [#1893] - `dev::ServiceRequest::from_request` can no longer fail. [#1893] - `dev::ServiceResponse::error_response()` now uses body type of `BoxBody`. [#2201] - `dev::ServiceResponse::map_body()` closure receives and returns `B` instead of `ResponseBody`. [#2201] - `http::header::ContentType::html()` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] - `HttpRequest::url_for`'s constructed URLs no longer contain query or fragment. [#2430] - `HttpResponseBuilder::json()` can now receive data by value and reference. [#1903] - `HttpServer::{listen_rustls, bind_rustls}()` now honor the ALPN protocols in the configuration parameter. [#2226] - `middleware::NormalizePath()` now will not try to normalize URIs with no valid path [#2246] - `test::TestRequest::param()` now accepts more than just static strings. [#2172] - `web::Data::into_inner()` and `Data::get_ref()` no longer require `T: Sized`. [#2403] - Rename `HttpServer::{client_timeout => client_request_timeout}()`. [#2611] - Rename `HttpServer::{client_shutdown => client_disconnect_timeout}()`. [#2611] - Rename `http::header::Accept::{mime_precedence => ranked}()`. [#2480] - Rename `http::header::Accept::{mime_preference => preference}()`. [#2480] - Rename `middleware::DefaultHeaders::{content_type => add_content_type}()`. [#1875] - Rename `dev::ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] - Trait Implementations: - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] - Misc: - Maximum number of handler extractors has increased to 12. [#2582] - The default `TrailingSlash` behavior is now `Trim`, in line with existing documentation. See migration guide for implications. [#1875] - `Result` extractor wrapper can now convert error types. [#2581] - Compress middleware will return `406 Not Acceptable` when no content encoding is acceptable to the client. [#2344] - Adjusted default JSON payload limit to 2MB (from 32kb). [#2162] - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] - Improve spec compliance of `dev::ConnectionInfo` extractor. [#2282] - Associated types in `FromRequest` implementation for `Option` and `Result` have changed. [#2581] - Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] - Scope and Resource middleware can access data items set on their own layer. [#2288] - Multiple calls to `App::data()` with the same type now keeps the latest call's data. [#1906] - Typed headers containing lists that require one or more items now enforce this minimum. [#2482] - `dev::ConnectionInfo::peer_addr` will no longer return the port number. [#2554] - `dev::ConnectionInfo::realip_remote_addr` will no longer return the port number if sourcing the IP from the peer's socket address. [#2554] - Accept wildcard `*` items in `AcceptLanguage`. [#2480] - Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] - Fix quality parse error in `http::header::AcceptEncoding` typed header. [#2344] - Double ampersand in `middleware::Logger` format is escaped correctly. [#2067] - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Security - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ### Removed - Crate Features: - `compress` feature. [#2065] - Functions: - `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] - `test::start_with`; moved to new `actix-test` crate. [#2112] - `test::start`; moved to new `actix-test` crate. [#2112] - `test::unused_addr`; moved to new `actix-test` crate. [#2112] - Traits: - `BodyEncoding`; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] - Types: - `dev::{BodySize, MessageBody, SizedStream}` re-exports; they are exposed through the `body` module. [#2468] - `EitherExtractError` direct export. [#2510] - `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] - `test::TestServer`; moved to new `actix-test` crate. [#2112] - `test::TestServerConfig`; moved to new `actix-test` crate. [#2112] - `web::HttpRequest` re-export. [#2663] - `web::HttpResponse` re-export. [#2663] - Methods: - `AppService::set_service_data`; for custom HTTP service factories adding application data, use the layered data model by calling `ServiceRequest::add_data_container` when handling requests instead. [#1906] - `dev::ConnectionInfo::get`. [#2487] - `dev::ServiceResponse::checked_expr`. [#2401] - `HttpRequestBuilder::del_cookie`. [#2591] - `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] - `HttpResponseBuilder::json2()`. [#1903] - `middleware::Compress::new`; restricting compression algorithm is done through feature flags. [#2501] - `test::TestRequest::with_header()`; use `test::TestRequest::default().insert_header()`. [#1869] - Trait Implementations: - Implementation of `From` for `Either` crate. [#2516] - Implementation of `Future` for `HttpResponse`. [#2601] - Misc: - The `client` module was removed; use the `awc` crate directly. [871ca5e4] - `middleware::{normalize, err_handlers}` modules; all necessary middleware types are now exposed in the `middleware` module. [#1812]: https://github.com/actix/actix-web/pull/1812 [#1813]: https://github.com/actix/actix-web/pull/1813 [#1852]: https://github.com/actix/actix-web/pull/1852 [#1865]: https://github.com/actix/actix-web/pull/1865 [#1869]: https://github.com/actix/actix-web/pull/1869 [#1875]: https://github.com/actix/actix-web/pull/1875 [#1878]: https://github.com/actix/actix-web/pull/1878 [#1891]: https://github.com/actix/actix-web/pull/1891 [#1893]: https://github.com/actix/actix-web/pull/1893 [#1894]: https://github.com/actix/actix-web/pull/1894 [#1903]: https://github.com/actix/actix-web/pull/1903 [#1905]: https://github.com/actix/actix-web/pull/1905 [#1906]: https://github.com/actix/actix-web/pull/1906 [#1933]: https://github.com/actix/actix-web/pull/1933 [#1957]: https://github.com/actix/actix-web/pull/1957 [#1957]: https://github.com/actix/actix-web/pull/1957 [#1981]: https://github.com/actix/actix-web/pull/1981 [#1988]: https://github.com/actix/actix-web/pull/1988 [#2010]: https://github.com/actix/actix-web/pull/2010 [#2065]: https://github.com/actix/actix-web/pull/2065 [#2067]: https://github.com/actix/actix-web/pull/2067 [#2093]: https://github.com/actix/actix-web/pull/2093 [#2094]: https://github.com/actix/actix-web/pull/2094 [#2097]: https://github.com/actix/actix-web/pull/2097 [#2112]: https://github.com/actix/actix-web/pull/2112 [#2148]: https://github.com/actix/actix-web/pull/2148 [#2162]: https://github.com/actix/actix-web/pull/2162 [#2172]: https://github.com/actix/actix-web/pull/2172 [#2177]: https://github.com/actix/actix-web/pull/2177 [#2200]: https://github.com/actix/actix-web/pull/2200 [#2201]: https://github.com/actix/actix-web/pull/2201 [#2201]: https://github.com/actix/actix-web/pull/2201 [#2233]: https://github.com/actix/actix-web/pull/2233 [#2246]: https://github.com/actix/actix-web/pull/2246 [#2250]: https://github.com/actix/actix-web/pull/2250 [#2253]: https://github.com/actix/actix-web/pull/2253 [#2262]: https://github.com/actix/actix-web/pull/2262 [#2263]: https://github.com/actix/actix-web/pull/2263 [#2271]: https://github.com/actix/actix-web/pull/2271 [#2282]: https://github.com/actix/actix-web/pull/2282 [#2288]: https://github.com/actix/actix-web/pull/2288 [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2379]: https://github.com/actix/actix-web/pull/2379 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 [#2403]: https://github.com/actix/actix-web/pull/2403 [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 [#2423]: https://github.com/actix/actix-web/pull/2423 [#2430]: https://github.com/actix/actix-web/pull/2430 [#2442]: https://github.com/actix/actix-web/pull/2442 [#2446]: https://github.com/actix/actix-web/pull/2446 [#2448]: https://github.com/actix/actix-web/pull/2448 [#2468]: https://github.com/actix/actix-web/pull/2468 [#2474]: https://github.com/actix/actix-web/pull/2474 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 [#2484]: https://github.com/actix/actix-web/pull/2484 [#2485]: https://github.com/actix/actix-web/pull/2485 [#2487]: https://github.com/actix/actix-web/pull/2487 [#2491]: https://github.com/actix/actix-web/pull/2491 [#2492]: https://github.com/actix/actix-web/pull/2492 [#2493]: https://github.com/actix/actix-web/pull/2493 [#2499]: https://github.com/actix/actix-web/pull/2499 [#2501]: https://github.com/actix/actix-web/pull/2501 [#2510]: https://github.com/actix/actix-web/pull/2510 [#2515]: https://github.com/actix/actix-web/pull/2515 [#2516]: https://github.com/actix/actix-web/pull/2516 [#2518]: https://github.com/actix/actix-web/pull/2518 [#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 [#2552]: https://github.com/actix/actix-web/pull/2552 [#2554]: https://github.com/actix/actix-web/pull/2554 [#2555]: https://github.com/actix/actix-web/pull/2555 [#2565]: https://github.com/actix/actix-web/pull/2565 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 [#2581]: https://github.com/actix/actix-web/pull/2581 [#2582]: https://github.com/actix/actix-web/pull/2582 [#2584]: https://github.com/actix/actix-web/pull/2584 [#2585]: https://github.com/actix/actix-web/pull/2585 [#2586]: https://github.com/actix/actix-web/pull/2586 [#2591]: https://github.com/actix/actix-web/pull/2591 [#2594]: https://github.com/actix/actix-web/pull/2594 [#2601]: https://github.com/actix/actix-web/pull/2601 [#2611]: https://github.com/actix/actix-web/pull/2611 [#2619]: https://github.com/actix/actix-web/pull/2619 [#2625]: https://github.com/actix/actix-web/pull/2625 [#2635]: https://github.com/actix/actix-web/pull/2635 [#2659]: https://github.com/actix/actix-web/pull/2659 [#2663]: https://github.com/actix/actix-web/pull/2663 [871ca5e4]: https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7
4.0.0 Pre-Releases ## 4.0.0-rc.3 ### Changed - `middleware::Condition` gained a broader compatibility; `Compat` is needed in fewer cases. [#2635] ### Added - Implement `Responder` for `Vec`. [#2625] - Re-export `KeepAlive` in `http` mod. [#2625] [#2625]: https://github.com/actix/actix-web/pull/2625 [#2635]: https://github.com/actix/actix-web/pull/2635 ## 4.0.0-rc.2 ### Added - On-by-default `macros` feature flag to enable routing and runtime macros. [#2619] ### Removed - `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] [#2619]: https://github.com/actix/actix-web/pull/2619 ## 4.0.0-rc.1 ### Changed - Rename `HttpServer::{client_timeout => client_request_timeout}`. [#2611] - Rename `HttpServer::{client_shutdown => client_disconnect_timeout}`. [#2611] ### Removed - `impl Future for HttpResponse`. [#2601] [#2601]: https://github.com/actix/actix-web/pull/2601 [#2611]: https://github.com/actix/actix-web/pull/2611 ## 4.0.0-beta.21 ### Added - `HttpResponse::add_removal_cookie`. [#2586] - `Logger::log_target`. [#2594] ### Removed - `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] - `HttpRequestBuilder::del_cookie`. [#2591] [#2585]: https://github.com/actix/actix-web/pull/2585 [#2586]: https://github.com/actix/actix-web/pull/2586 [#2591]: https://github.com/actix/actix-web/pull/2591 [#2594]: https://github.com/actix/actix-web/pull/2594 ## 4.0.0-beta.20 ### Added - `GuardContext::header` [#2569] - `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] ### Changed - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] - `Result` extractor wrapper can now convert error types. [#2581] - Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] - Maximum number of handler extractors has increased to 12. [#2582] - Removed bound `::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584] [#1988]: https://github.com/actix/actix-web/pull/1988 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 [#2581]: https://github.com/actix/actix-web/pull/2581 [#2582]: https://github.com/actix/actix-web/pull/2582 [#2584]: https://github.com/actix/actix-web/pull/2584 ## 4.0.0-beta.19 ### Added - `impl Hash` for `http::header::Encoding`. [#2501] - `AcceptEncoding::negotiate()`. [#2501] ### Changed - `AcceptEncoding::preference` now returns `Option>`. [#2501] - Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] - `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] ### Fixed - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] ### Removed - `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] - `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 ## 4.0.0-beta.18 ### Changed - Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Minimum supported Rust version (MSRV) is now 1.54. ### Security - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [#2555]: https://github.com/actix/actix-web/pull/2555 [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ## 4.0.0-beta.17 ### Added - `guard::GuardContext` for use with the `Guard` trait. [#2552] - `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] ### Changed - `Guard` trait now receives a `&GuardContext`. [#2552] - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] - The `Not` guard is now generic over the type of guard it wraps. [#2552] ### Fixed - Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] - `ConnectionInfo::peer_addr` will not return the port number. [#2554] - `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554] [#2552]: https://github.com/actix/actix-web/pull/2552 [#2554]: https://github.com/actix/actix-web/pull/2554 ## 4.0.0-beta.16 ### Changed - No longer require `Scope` service body type to be boxed. [#2523] - No longer require `Resource` service body type to be boxed. [#2526] [#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 ## 4.0.0-beta.15 ### Added - Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510] - Implement `Debug` for `DefaultHeaders`. [#2510] ### Changed - Align `DefaultHeader` method terminology, deprecating previous methods. [#2510] - Response service types in `ErrorHandlers` middleware now use `ServiceResponse>` to allow changing the body type. [#2515] - Both variants in `ErrorHandlerResponse` now use `ServiceResponse>`. [#2515] - Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518] - Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518] - Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518] - Relax body type and error bounds on test utilities. [#2518] ### Removed - Top-level `EitherExtractError` export. [#2510] - Conversion implementations for `either` crate. [#2516] - `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] [#2510]: https://github.com/actix/actix-web/pull/2510 [#2515]: https://github.com/actix/actix-web/pull/2515 [#2516]: https://github.com/actix/actix-web/pull/2516 [#2518]: https://github.com/actix/actix-web/pull/2518 ## 4.0.0-beta.14 ### Added - Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] - `AcceptEncoding` typed header. [#2482] - `Range` typed header. [#2485] - `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] - `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] - Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] - `HttpRequest::{req_data,req_data_mut}`. [#2487] - `ServiceResponse::into_parts`. [#2499] ### Changed - Rename `Accept::{mime_precedence => ranked}`. [#2480] - Rename `Accept::{mime_preference => preference}`. [#2480] - Un-deprecate `App::data_factory`. [#2484] - `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430] - Remove `B` (body) type parameter on `App`. [#2493] - Add `B` (body) type parameter on `Scope`. [#2492] - Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] ### Fixed - Accept wildcard `*` items in `AcceptLanguage`. [#2480] - Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] - Typed headers containing lists that require one or more items now enforce this minimum. [#2482] ### Removed - `ConnectionInfo::get`. [#2487] [#2430]: https://github.com/actix/actix-web/pull/2430 [#2468]: https://github.com/actix/actix-web/pull/2468 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 [#2484]: https://github.com/actix/actix-web/pull/2484 [#2485]: https://github.com/actix/actix-web/pull/2485 [#2487]: https://github.com/actix/actix-web/pull/2487 [#2491]: https://github.com/actix/actix-web/pull/2491 [#2492]: https://github.com/actix/actix-web/pull/2492 [#2493]: https://github.com/actix/actix-web/pull/2493 [#2499]: https://github.com/actix/actix-web/pull/2499 ## 4.0.0-beta.13 ### Changed - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 ## 4.0.0-beta.12 ### Changed - Compress middleware's response type is now `AnyBody>`. [#2448] ### Fixed - Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] ### Removed - `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 [#2448]: https://github.com/actix/actix-web/pull/2448 ## 4.0.0-beta.11 ### Added - Re-export `dev::ServerHandle` from `actix-server`. [#2442] ### Changed - `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] - Update `actix-server` to `2.0.0-beta.9`. [#2442] [#2423]: https://github.com/actix/actix-web/pull/2423 [#2442]: https://github.com/actix/actix-web/pull/2442 ## 4.0.0-beta.10 ### Added - Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] - `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] ### Changed - Associated type `FromRequest::Config` was removed. [#2233] - Inner field made private on `web::Payload`. [#2384] - `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] - Updated rustls to v0.20. [#2414] - Minimum supported Rust version (MSRV) is now 1.52. ### Removed - Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 [#2403]: https://github.com/actix/actix-web/pull/2403 [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 ## 4.0.0-beta.9 ### Added - Re-export actix-service `ServiceFactory` in `dev` module. [#2325] ### Changed - Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] - Move `BaseHttpResponse` to `dev::Response`. [#2379] - Enable `TestRequest::param` to accept more than just static strings. [#2172] - Minimum supported Rust version (MSRV) is now 1.51. ### Fixed - Fix quality parse error in Accept-Encoding header. [#2344] - Re-export correct type at `web::HttpResponse`. [#2379] [#2172]: https://github.com/actix/actix-web/pull/2172 [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 [#2379]: https://github.com/actix/actix-web/pull/2379 ## 4.0.0-beta.8 ### Added - Add `ServiceRequest::parts_mut`. [#2177] - Add extractors for `Uri` and `Method`. [#2263] - Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] - Add `Route::service` for using hand-written services as handlers. [#2262] ### Changed - Change compression algorithm features flags. [#2250] - Deprecate `App::data` and `App::data_factory`. [#2271] - Smarter extraction of `ConnectionInfo` parts. [#2282] ### Fixed - Scope and Resource middleware can access data items set on their own layer. [#2288] [#2177]: https://github.com/actix/actix-web/pull/2177 [#2250]: https://github.com/actix/actix-web/pull/2250 [#2271]: https://github.com/actix/actix-web/pull/2271 [#2262]: https://github.com/actix/actix-web/pull/2262 [#2263]: https://github.com/actix/actix-web/pull/2263 [#2282]: https://github.com/actix/actix-web/pull/2282 [#2288]: https://github.com/actix/actix-web/pull/2288 ## 4.0.0-beta.7 ### Added - `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] ### Changed - Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] - `ServiceResponse::error_response` now uses body type of `Body`. [#2201] - `ServiceResponse::checked_expr` now returns a `Result`. [#2201] - Update `language-tags` to `0.3`. - `ServiceResponse::take_body`. [#2201] - `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` types. [#2201] - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] - `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226] - `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] ### Removed - `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] [#2162]: https://github.com/actix/actix-web/pull/2162 [#2200]: https://github.com/actix/actix-web/pull/2200 [#2201]: https://github.com/actix/actix-web/pull/2201 [#2253]: https://github.com/actix/actix-web/pull/2253 [#2246]: https://github.com/actix/actix-web/pull/2246 ## 4.0.0-beta.6 ### Added - `HttpResponse` and `HttpResponseBuilder` types. [#2065] ### Changed - Most error types are now marked `#[non_exhaustive]`. [#2148] - Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. [#2065]: https://github.com/actix/actix-web/pull/2065 [#2148]: https://github.com/actix/actix-web/pull/2148 ## 4.0.0-beta.5 ### Added - `Header` extractor for extracting common HTTP headers in handlers. [#2094] - Added `TestServer::client_headers` method. [#2097] ### Changed - `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed instead of skipping. (Only the first error is kept when multiple error occur) [#2093] ### Fixed - Double ampersand in Logger format is escaped correctly. [#2067] ### Removed - The `client` mod was removed. Clients should now use `awc` directly. [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) - Integration testing was moved to new `actix-test` crate. Namely these items from the `test` module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] [#2067]: https://github.com/actix/actix-web/pull/2067 [#2093]: https://github.com/actix/actix-web/pull/2093 [#2094]: https://github.com/actix/actix-web/pull/2094 [#2097]: https://github.com/actix/actix-web/pull/2097 [#2112]: https://github.com/actix/actix-web/pull/2112 ## 4.0.0-beta.4 ### Changed - Feature `cookies` is now optional and enabled by default. [#1981] - `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default behaviour of the `web::Json` extractor. [#2010] [#1981]: https://github.com/actix/actix-web/pull/1981 [#2010]: https://github.com/actix/actix-web/pull/2010 ## 4.0.0-beta.3 - Update `actix-web-codegen` to `0.5.0-beta.1`. ## 4.0.0-beta.2 ### Added - The method `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] - Add `services!` macro for helping register multiple services to `App`. [#1933] - Enable registering a vec of services of the same type to `App` [#1933] ### Changed - Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. Making it simpler and more performant. [#1891] - `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] - `ServiceRequest::from_request` can no longer fail. [#1893] - Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] - `test::{call_service, read_response, read_response_json, send_request}` take `&Service` in argument [#1905] - `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure argument. [#1905] - `web::block` no longer requires the output is a Result. [#1957] ### Fixed - Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] ### Removed - Public field of `web::Path` has been made private. [#1894] - Public field of `web::Query` has been made private. [#1894] - `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] - `AppService::set_service_data`; for custom HTTP service factories adding application data, use the layered data model by calling `ServiceRequest::add_data_container` when handling requests instead. [#1906] [#1891]: https://github.com/actix/actix-web/pull/1891 [#1893]: https://github.com/actix/actix-web/pull/1893 [#1894]: https://github.com/actix/actix-web/pull/1894 [#1869]: https://github.com/actix/actix-web/pull/1869 [#1905]: https://github.com/actix/actix-web/pull/1905 [#1906]: https://github.com/actix/actix-web/pull/1906 [#1933]: https://github.com/actix/actix-web/pull/1933 [#1957]: https://github.com/actix/actix-web/pull/1957 ## 4.0.0-beta.1 ### Added - `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] ### Changed - Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] - Bumped `rand` to `0.8`. - Update `rust-tls` to `0.19`. [#1813] - Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] - The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration guide for implications. [#1875] - Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] - MSRV is now 1.46.0. ### Fixed - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Removed - Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now exposed directly by the `middleware` module. - Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported from `actix_web::error` module. [#1878] [#1812]: https://github.com/actix/actix-web/pull/1812 [#1813]: https://github.com/actix/actix-web/pull/1813 [#1852]: https://github.com/actix/actix-web/pull/1852 [#1865]: https://github.com/actix/actix-web/pull/1865 [#1875]: https://github.com/actix/actix-web/pull/1875 [#1878]: https://github.com/actix/actix-web/pull/1878
## 3.3.3 ### Changed - Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529] [#2529]: https://github.com/actix/actix-web/pull/2529 ## 3.3.2 ### Fixed - Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762] - Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798] - Increase minimum `socket2` version. [#1803] [#1762]: https://github.com/actix/actix-web/pull/1762 [#1798]: https://github.com/actix/actix-web/pull/1798 [#1803]: https://github.com/actix/actix-web/pull/1803 ## 3.3.1 - Ensure `actix-http` dependency uses same `serde_urlencoded`. ## 3.3.0 ### Added - Add `Either` extractor helper. [#1788] ### Changed - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 [#1788]: https://github.com/actix/actix-web/pull/1788 ## 3.2.0 ### Added - Implement `exclude_regex` for Logger middleware. [#1723] - Add request-local data extractor `web::ReqData`. [#1748] - Add ability to register closure for request middleware logging. [#1749] - Add `app_data` to `ServiceConfig`. [#1757] - Expose `on_connect` for access to the connection stream before request is handled. [#1754] ### Changed - Updated `actix-web-codegen` dependency for access to new `#[route(...)]` multi-method macro. - Print non-configured `Data` type when attempting extraction. [#1743] - Re-export `bytes::Buf{Mut}` in web module. [#1750] - Upgrade `pin-project` to `1.0`. [#1723]: https://github.com/actix/actix-web/pull/1723 [#1743]: https://github.com/actix/actix-web/pull/1743 [#1748]: https://github.com/actix/actix-web/pull/1748 [#1750]: https://github.com/actix/actix-web/pull/1750 [#1754]: https://github.com/actix/actix-web/pull/1754 [#1757]: https://github.com/actix/actix-web/pull/1757 [#1749]: https://github.com/actix/actix-web/pull/1749 ## 3.1.0 ### Changed - Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` to retain any trailing slashes. [#1695] - Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` via `web::Data::from` [#1710] ### Fixed - `ResourceMap` debug printing is no longer infinitely recursive. [#1708] [#1695]: https://github.com/actix/actix-web/pull/1695 [#1708]: https://github.com/actix/actix-web/pull/1708 [#1710]: https://github.com/actix/actix-web/pull/1710 ## 3.0.2 ### Fixed - `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] [#1678]: https://github.com/actix/actix-web/pull/1678 ## 3.0.1 ### Changed - `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] [#1673]: https://github.com/actix/actix-web/pull/1673 ## 3.0.0 - No significant changes from `3.0.0-beta.4`. ## 3.0.0-beta.4 ### Added - `middleware::NormalizePath` now has configurable behavior for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed - Update actix-codec and actix-utils dependencies. [#1634] - `FormConfig` and `JsonConfig` configurations are now also considered when set using `App::data`. [#1641] - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] - `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. [#1655] [#1639]: https://github.com/actix/actix-web/pull/1639 [#1641]: https://github.com/actix/actix-web/pull/1641 [#1634]: https://github.com/actix/actix-web/pull/1634 [#1655]: https://github.com/actix/actix-web/pull/1655 ## 3.0.0-beta.3 ### Changed - Update `rustls` to 0.18 ## 3.0.0-beta.2 ### Changed - `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set using `App::data`. [#1610] - `web::Path` now has a public representation: `web::Path(pub T)` that enables destructuring. [#1594] - `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to access `HttpRequest` which already allows this. [#1618] - Re-export all error types from `awc`. [#1621] - MSRV is now 1.42.0. ### Fixed - Memory leak of app data in pooled requests. [#1609] [#1594]: https://github.com/actix/actix-web/pull/1594 [#1609]: https://github.com/actix/actix-web/pull/1609 [#1610]: https://github.com/actix/actix-web/pull/1610 [#1618]: https://github.com/actix/actix-web/pull/1618 [#1621]: https://github.com/actix/actix-web/pull/1621 ## 3.0.0-beta.1 ### Added - Re-export `actix_rt::main` as `actix_web::main`. - `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. - `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed - Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] - Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. - MSRV is now 1.41.1 ### Fixed - `NormalizePath` improved consistency when path needs slashes added _and_ removed. ## 3.0.0-alpha.3 ### Added - Add option to create `Data` from `Arc` [#1509] ### Changed - Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] - Fix audit issue logging by default peer address [#1485] - Bump minimum supported Rust version to 1.40 - Replace deprecated `net2` crate with `socket2` [#1485]: https://github.com/actix/actix-web/pull/1485 [#1509]: https://github.com/actix/actix-web/pull/1509 ## 3.0.0-alpha.2 ### Changed - `{Resource,Scope}::default_service(f)` handlers now support app data extraction. [#1452] - Implement `std::error::Error` for our custom errors [#1422] - NormalizePath middleware now appends trailing / so that routes of form /example/ respond to /example requests. [#1433] - Remove the `failure` feature and support. [#1422]: https://github.com/actix/actix-web/pull/1422 [#1433]: https://github.com/actix/actix-web/pull/1433 [#1452]: https://github.com/actix/actix-web/pull/1452 [#1486]: https://github.com/actix/actix-web/pull/1486 ## 3.0.0-alpha.1 ### Added - Add helper function for creating routes with `TRACE` method guard `web::trace()` - Add convenience functions `test::read_body_json()` and `test::TestRequest::send_request()` for testing. ### Changed - Use `sha-1` crate instead of unmaintained `sha1` crate - Skip empty chunks when returning response from a `Stream` [#1308] - Update the `time` dependency to 0.2.7 - Update `actix-tls` dependency to 2.0.0-alpha.1 - Update `rustls` dependency to 0.17 [#1308]: https://github.com/actix/actix-web/pull/1308 ## 2.0.0 ### Changed - Rename `HttpServer::start()` to `HttpServer::run()` - Allow to gracefully stop test server via `TestServer::stop()` - Allow to specify multi-patterns for resources ## 2.0.0-rc ### Changed - Move `BodyEncoding` to `dev` module #1220 - Allow to set `peer_addr` for TestRequest #1074 - Make web::Data deref to Arc #1214 - Rename `App::register_data()` to `App::app_data()` - `HttpRequest::app_data()` returns `Option<&T>` instead of `Option<&Data>` ### Fixed - Fix `AppConfig::secure()` is always false. #1202 ## 2.0.0-alpha.6 ### Fixed - Fixed compilation with default features off ## 2.0.0-alpha.5 ### Added - Add test server, `test::start()` and `test::start_with()` ## 2.0.0-alpha.4 ### Deleted - Delete HttpServer::run(), it is not useful with async/await ## 2.0.0-alpha.3 ### Changed - Migrate to tokio 0.2 ## 2.0.0-alpha.1 ### Changed - Migrated to `std::future` - Remove implementation of `Responder` for `()`. (#1167) ## 1.0.9 ### Added - Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110) ### Changed - Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129) ## 1.0.8 ### Added - Add `Scope::register_data` and `Resource::register_data` methods, parallel to `App::register_data`. - Add `middleware::Condition` that conditionally enables another middleware - Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` - Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, which is useful for example with systemd. ### Changed - Make UrlEncodedError::Overflow more informative - Use actix-testing for testing utils ## 1.0.7 ### Fixed - Request Extensions leak #1062 ## 1.0.6 ### Added - Re-implement Host predicate (#989) - Form implements Responder, returning a `application/x-www-form-urlencoded` response - Add `into_inner` to `Data` - Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set the header in test requests. ### Changed - `Query` payload made `pub`. Allows user to pattern-match the payload. - Enable `rust-tls` feature for client #1045 - Update serde_urlencoded to 0.6.1 - Update url to 2.1 ## 1.0.5 ### Added - Unix domain sockets (HttpServer::bind_uds) #92 - Actix now logs errors resulting in "internal server error" responses always, with the `error` logging level ### Fixed - Restored logging of errors through the `Logger` middleware ## 1.0.4 ### Added - Add `Responder` impl for `(T, StatusCode) where T: Responder` - Allow to access app's resource map via `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. ### Changed - Upgrade `rand` dependency version to 0.7 ## 1.0.3 ### Added - Support asynchronous data factories #850 ### Changed - Use `encoding_rs` crate instead of unmaintained `encoding` crate ## 1.0.2 ### Changed - Move cors middleware to `actix-cors` crate. - Move identity middleware to `actix-identity` crate. ## 1.0.1 ### Added - Add support for PathConfig #903 - Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`. ### Changed - Move cors middleware to `actix-cors` crate. - Move identity middleware to `actix-identity` crate. - Disable default feature `secure-cookies`. - Allow to test an app that uses async actors #897 - Re-apply patch from #637 #894 ### Fixed - HttpRequest::url_for is broken with nested scopes #915 ## 1.0.0 ### Added - Add `Scope::configure()` method. - Add `ServiceRequest::set_payload()` method. - Add `test::TestRequest::set_json()` convenience method to automatically serialize data and set header in test requests. - Add macros for head, options, trace, connect and patch http methods ### Changed - Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 ### Fixed - Fix Logger request time format, and use rfc3339. #867 - Clear http requests pool on app service drop #860 ## 1.0.0-rc ### Added - Add `Query::from_query()` to extract parameters from a query string. #846 - `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. ### Changed - `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too. ### Fixed - Codegen with parameters in the path only resolves the first registered endpoint #841 ## 1.0.0-beta.4 ### Added - Allow to set/override app data on scope level ### Changed - `App::configure` take an `FnOnce` instead of `Fn` - Upgrade actix-net crates ## 1.0.0-beta.3 ### Added - Add helper function for executing futures `test::block_fn()` ### Changed - Extractor configuration could be registered with `App::data()` or with `Resource::data()` #775 - Route data is unified with app data, `Route::data()` moved to resource level to `Resource::data()` - CORS handling without headers #702 - Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. ### Fixed - Fix `NormalizePath` middleware impl #806 ### Deleted - `App::data_factory()` is deleted. ## 1.0.0-beta.2 ### Added - Add raw services support via `web::service()` - Add helper functions for reading response body `test::read_body()` - Add support for `remainder match` (i.e "/path/{tail}\*") - Extend `Responder` trait, allow to override status code and headers. - Store visit and login timestamp in the identity cookie #502 ### Changed - `.to_async()` handler can return `Responder` type #792 ### Fixed - Fix async web::Data factory handling ## 1.0.0-beta.1 ### Added - Add helper functions for reading test response body, `test::read_response()` and test::read_response_json()` - Add `.peer_addr()` #744 - Add `NormalizePath` middleware ### Changed - Rename `RouterConfig` to `ServiceConfig` - Rename `test::call_success` to `test::call_service` - Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. - `CookieIdentityPolicy::max_age()` accepts value in seconds ### Fixed - Fixed `TestRequest::app_data()` ## 1.0.0-alpha.6 ### Changed - Allow using any service as default service. - Remove generic type for request payload, always use default. - Removed `Decompress` middleware. Bytes, String, Json, Form extractors automatically decompress payload. - Make extractor config type explicit. Add `FromRequest::Config` associated type. ## 1.0.0-alpha.5 ### Added - Added async io `TestBuffer` for testing. ### Deleted - Removed native-tls support ## 1.0.0-alpha.4 ### Added - `App::configure()` allow to offload app configuration to different methods - Added `URLPath` option for logger - Added `ServiceRequest::app_data()`, returns `Data` - Added `ServiceFromRequest::app_data()`, returns `Data` ### Changed - `FromRequest` trait refactoring - Move multipart support to actix-multipart crate ### Fixed - Fix body propagation in Response::from_error. #760 ## 1.0.0-alpha.3 ### Changed - Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()` - Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` - Removed `Deref` impls ### Removed - Removed unused `actix_web::web::md()` ## 1.0.0-alpha.2 ### Added - Rustls support ### Changed - Use forked cookie - Multipart::Field renamed to MultipartField ## 1.0.0-alpha.1 ### Changed - Complete architecture re-design. - Return 405 response if no matching route found within resource #538 actix-web-4.9.0/Cargo.lock0000644000002064550000000000100107200ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "actix-codec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", "memchr", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-files" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" dependencies = [ "actix-http", "actix-service", "actix-utils", "actix-web 4.8.0", "bitflags 2.6.0", "bytes", "derive_more", "futures-core", "http-range", "log", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "v_htmlescape", ] [[package]] name = "actix-http" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-tls", "actix-utils", "ahash", "base64 0.22.1", "bitflags 2.6.0", "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", "futures-core", "h2", "http 0.2.12", "httparse", "httpdate", "itoa", "language-tags", "local-channel", "mime", "percent-encoding", "pin-project-lite", "rand", "sha1", "smallvec", "tokio", "tokio-util", "tracing", "zstd", ] [[package]] name = "actix-http-test" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061d27c2a6fea968fdaca0961ff429d23a4ec878c4f68f5d08626663ade69c80" dependencies = [ "actix-codec", "actix-rt", "actix-server", "actix-service", "actix-tls", "actix-utils", "awc", "bytes", "futures-core", "http 0.2.12", "log", "serde", "serde_json", "serde_urlencoded", "slab", "socket2 0.5.7", "tokio", ] [[package]] name = "actix-macros" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", "syn", ] [[package]] name = "actix-router" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", "http 0.2.12", "regex", "regex-lite", "serde", "tracing", ] [[package]] name = "actix-rt" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "actix-macros", "futures-core", "tokio", "tokio-uring", ] [[package]] name = "actix-server" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", "mio", "socket2 0.5.7", "tokio", "tokio-uring", "tracing", ] [[package]] name = "actix-service" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" dependencies = [ "futures-core", "paste", "pin-project-lite", ] [[package]] name = "actix-test" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439022b5a7b5dac10798465029a9566e8e0cca7a6014541ed277b695691fac5f" dependencies = [ "actix-codec", "actix-http", "actix-http-test", "actix-rt", "actix-service", "actix-utils", "actix-web 4.8.0", "awc", "futures-core", "futures-util", "log", "openssl", "rustls 0.23.12", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "actix-tls" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "http 0.2.12", "http 1.1.0", "impl-more", "openssl", "pin-project-lite", "rustls-pki-types", "tokio", "tokio-openssl", "tokio-rustls 0.23.4", "tokio-rustls 0.24.1", "tokio-rustls 0.25.0", "tokio-rustls 0.26.0", "tokio-util", "tracing", "webpki-roots 0.22.6", "webpki-roots 0.25.4", "webpki-roots 0.26.3", ] [[package]] name = "actix-utils" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", "pin-project-lite", ] [[package]] name = "actix-web" version = "4.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" dependencies = [ "actix-codec", "actix-http", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", "ahash", "bytes", "bytestring", "cfg-if", "cookie", "derive_more", "encoding_rs", "futures-core", "futures-util", "itoa", "language-tags", "log", "mime", "once_cell", "pin-project-lite", "regex-lite", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2 0.5.7", "time", "url", ] [[package]] name = "actix-web" version = "4.9.0" dependencies = [ "actix-codec", "actix-files", "actix-http", "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-test", "actix-tls", "actix-utils", "actix-web-codegen", "ahash", "awc", "brotli", "bytes", "bytestring", "cfg-if", "const-str", "cookie", "core_affinity", "criterion", "derive_more", "encoding_rs", "env_logger", "flate2", "futures-core", "futures-util", "impl-more", "itoa", "language-tags", "log", "mime", "once_cell", "openssl", "pin-project-lite", "rand", "rcgen", "regex", "regex-lite", "rustls 0.23.12", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2 0.5.7", "static_assertions", "time", "tokio", "url", "zstd", ] [[package]] name = "actix-web-codegen" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", "quote", "syn", ] [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", "generic-array", ] [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", "cipher", "ctr", "ghash", "subtle", ] [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "awc" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe6b67e44fb95d1dc9467e3930383e115f9b4ed60ca689db41409284e967a12d" dependencies = [ "actix-codec", "actix-http", "actix-rt", "actix-service", "actix-tls", "actix-utils", "base64 0.22.1", "bytes", "cfg-if", "cookie", "derive_more", "futures-core", "futures-util", "h2", "http 0.2.12", "itoa", "log", "mime", "openssl", "percent-encoding", "pin-project-lite", "rand", "rustls 0.23.12", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "aws-lc-rs" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" dependencies = [ "aws-lc-sys", "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", "libc", "paste", ] [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", "lazy_static", "lazycell", "log", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", "which", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "brotli" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "bytestring" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" dependencies = [ "bytes", ] [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" dependencies = [ "jobserver", "libc", ] [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "cmake" version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "const-str" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "aes-gcm", "base64 0.20.0", "hkdf", "hmac", "percent-encoding", "rand", "sha2", "subtle", "time", "version_check", ] [[package]] name = "core_affinity" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622892f5635ce1fc38c8f16dfc938553ed64af482edb5e150bf4caedbfcb2304" dependencies = [ "libc", "num_cpus", "winapi", ] [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "rand_core", "typenum", ] [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "flate2" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "ghash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-range" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" [[package]] name = "httparse" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "impl-more" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" [[package]] name = "indexmap" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "generic-array", ] [[package]] name = "io-uring" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] name = "is-terminal" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets", ] [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-channel" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", "local-waker", ] [[package]] name = "local-waker" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi", "libc", "log", "wasi", "windows-sys 0.52.0", ] [[package]] name = "mirai-annotations" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-sys" version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ "base64 0.22.1", "serde", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] [[package]] name = "polyval" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "prettyplease" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "rcgen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" dependencies = [ "pem", "ring 0.17.8", "rustls-pki-types", "time", "yasna", ] [[package]] name = "redox_syscall" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin 0.5.2", "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" dependencies = [ "log", "ring 0.16.20", "sct", "webpki", ] [[package]] name = "rustls" version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.6", "subtle", "zeroize", ] [[package]] name = "rustls" version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", "rustls-webpki 0.102.6", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "rustls-webpki" version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "aws-lc-rs", "ring 0.17.8", "rustls-pki-types", "untrusted 0.9.0", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-openssl" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" dependencies = [ "futures-util", "openssl", "openssl-sys", "tokio", ] [[package]] name = "tokio-rustls" version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", "tokio", "webpki", ] [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls 0.21.12", "tokio", ] [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls 0.22.4", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls 0.23.12", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-uring" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "748482e3e13584a34664a710168ad5068e8cb1d968aa4ffa887e83ca6dd27967" dependencies = [ "futures-util", "io-uring", "libc", "slab", "socket2 0.4.10", "tokio", ] [[package]] name = "tokio-util" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", ] [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "v_htmlescape" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "webpki-roots" version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] [[package]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] [[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", "home", "once_cell", "rustix", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ "time", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zstd" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", ] actix-web-4.9.0/Cargo.toml0000644000000176350000000000100107430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.72" name = "actix-web" version = "4.9.0" authors = [ "Nikolay Kim ", "Rob Ede ", ] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" homepage = "https://actix.rs" readme = "README.md" keywords = [ "actix", "http", "web", "framework", "async", ] categories = [ "network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket", ] license = "MIT OR Apache-2.0" repository = "https://github.com/actix/actix-web" [package.metadata.cargo_check_external_types] allowed_external_types = [ "actix_http::*", "actix_router::*", "actix_rt::*", "actix_server::*", "actix_service::*", "actix_utils::*", "actix_web_codegen::*", "bytes::*", "cookie::*", "cookie", "futures_core::*", "http::*", "language_tags::*", "mime::*", "openssl::*", "rustls::*", "serde_json::*", "serde_urlencoded::*", "serde::*", "serde::*", "tokio::*", "url::*", ] [package.metadata.docs.rs] features = [ "macros", "openssl", "rustls-0_20", "rustls-0_21", "rustls-0_22", "rustls-0_23", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies", ] rustdoc-args = [ "--cfg", "docsrs", ] [lib] name = "actix_web" path = "src/lib.rs" [[example]] name = "basic" path = "examples/basic.rs" required-features = ["compress-gzip"] [[example]] name = "macroless" path = "examples/macroless.rs" [[example]] name = "middleware_from_fn" path = "examples/middleware_from_fn.rs" [[example]] name = "on-connect" path = "examples/on-connect.rs" required-features = [] [[example]] name = "uds" path = "examples/uds.rs" required-features = ["compress-gzip"] [[example]] name = "worker-cpu-pin" path = "examples/worker-cpu-pin.rs" [[test]] name = "compression" path = "tests/compression.rs" required-features = [ "compress-brotli", "compress-gzip", "compress-zstd", ] [[test]] name = "test-macro-import-conflict" path = "tests/test-macro-import-conflict.rs" [[test]] name = "test_error_propagation" path = "tests/test_error_propagation.rs" [[test]] name = "test_httpserver" path = "tests/test_httpserver.rs" [[test]] name = "test_server" path = "tests/test_server.rs" required-features = [ "compress-brotli", "compress-gzip", "compress-zstd", "cookies", ] [[test]] name = "test_weird_poll" path = "tests/test_weird_poll.rs" [[test]] name = "utils" path = "tests/utils.rs" [[test]] name = "weird_poll" path = "tests/weird_poll.rs" [[bench]] name = "responder" path = "benches/responder.rs" harness = false [[bench]] name = "server" path = "benches/server.rs" harness = false [[bench]] name = "service" path = "benches/service.rs" harness = false [dependencies.actix-codec] version = "0.5" [dependencies.actix-http] version = "3.7" features = ["ws"] [dependencies.actix-macros] version = "0.2.3" optional = true [dependencies.actix-router] version = "0.5.3" features = ["http"] default-features = false [dependencies.actix-rt] version = "2.6" default-features = false [dependencies.actix-server] version = "2" [dependencies.actix-service] version = "2" [dependencies.actix-tls] version = "3.4" optional = true default-features = false [dependencies.actix-utils] version = "3" [dependencies.actix-web-codegen] version = "4.3" optional = true default-features = false [dependencies.ahash] version = "0.8" [dependencies.bytes] version = "1" [dependencies.bytestring] version = "1" [dependencies.cfg-if] version = "1" [dependencies.cookie] version = "0.16" features = ["percent-encode"] optional = true [dependencies.derive_more] version = "0.99.8" [dependencies.encoding_rs] version = "0.8" [dependencies.futures-core] version = "0.3.17" default-features = false [dependencies.futures-util] version = "0.3.17" default-features = false [dependencies.impl-more] version = "0.1.4" [dependencies.itoa] version = "1" [dependencies.language-tags] version = "0.3" [dependencies.log] version = "0.4" [dependencies.mime] version = "0.3" [dependencies.once_cell] version = "1.5" [dependencies.pin-project-lite] version = "0.2.7" [dependencies.regex] version = "1.5.5" optional = true [dependencies.regex-lite] version = "0.1" [dependencies.serde] version = "1.0" [dependencies.serde_json] version = "1.0" [dependencies.serde_urlencoded] version = "0.7" [dependencies.smallvec] version = "1.6.1" [dependencies.socket2] version = "0.5" [dependencies.time] version = "0.3" features = ["formatting"] default-features = false [dependencies.url] version = "2.1" [dev-dependencies.actix-files] version = "0.6" [dev-dependencies.actix-test] version = "0.1" features = [ "openssl", "rustls-0_23", ] [dev-dependencies.awc] version = "3" features = ["openssl"] [dev-dependencies.brotli] version = "6" [dev-dependencies.const-str] version = "0.5" [dev-dependencies.core_affinity] version = "0.8" [dev-dependencies.criterion] version = "0.5" features = ["html_reports"] [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.flate2] version = "1.0.13" [dev-dependencies.futures-util] version = "0.3.17" features = ["std"] default-features = false [dev-dependencies.rand] version = "0.8" [dev-dependencies.rcgen] version = "0.13" [dev-dependencies.rustls-pemfile] version = "2" [dev-dependencies.serde] version = "1.0" features = ["derive"] [dev-dependencies.static_assertions] version = "1" [dev-dependencies.tls-openssl] version = "0.10.55" package = "openssl" [dev-dependencies.tls-rustls] version = "0.23" package = "rustls" [dev-dependencies.tokio] version = "1.24.2" features = [ "rt-multi-thread", "macros", ] [dev-dependencies.zstd] version = "0.13" [features] __compress = [] __tls = [] compat = ["compat-routing-macros-force-pub"] compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"] compress-brotli = [ "actix-http/compress-brotli", "__compress", ] compress-gzip = [ "actix-http/compress-gzip", "__compress", ] compress-zstd = [ "actix-http/compress-zstd", "__compress", ] cookies = ["dep:cookie"] default = [ "macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2", "unicode", "compat", ] experimental-io-uring = ["actix-server/io-uring"] http2 = ["actix-http/http2"] macros = [ "dep:actix-macros", "dep:actix-web-codegen", ] openssl = [ "__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl", ] rustls = ["rustls-0_20"] rustls-0_20 = [ "__tls", "http2", "actix-http/rustls-0_20", "actix-tls/accept", "actix-tls/rustls-0_20", ] rustls-0_21 = [ "__tls", "http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls/rustls-0_21", ] rustls-0_22 = [ "__tls", "http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22", ] rustls-0_23 = [ "__tls", "http2", "actix-http/rustls-0_23", "actix-tls/accept", "actix-tls/rustls-0_23", ] secure-cookies = [ "cookies", "cookie/secure", ] unicode = [ "dep:regex", "actix-router/unicode", ] [lints.clippy] [lints.rust.future_incompatible] level = "deny" priority = 0 [lints.rust.nonstandard_style] level = "deny" priority = 0 [lints.rust.rust_2018_idioms] level = "deny" priority = 0 actix-web-4.9.0/Cargo.toml.orig000064400000000000000000000136111046102023000144120ustar 00000000000000[package] name = "actix-web" version = "4.9.0" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" authors = [ "Nikolay Kim ", "Rob Ede ", ] keywords = ["actix", "http", "web", "framework", "async"] categories = [ "network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket" ] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" license.workspace = true edition.workspace = true rust-version.workspace = true [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = [ "macros", "openssl", "rustls-0_20", "rustls-0_21", "rustls-0_22", "rustls-0_23", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies", ] [package.metadata.cargo_check_external_types] allowed_external_types = [ "actix_http::*", "actix_router::*", "actix_rt::*", "actix_server::*", "actix_service::*", "actix_utils::*", "actix_web_codegen::*", "bytes::*", "cookie::*", "cookie", "futures_core::*", "http::*", "language_tags::*", "mime::*", "openssl::*", "rustls::*", "serde_json::*", "serde_urlencoded::*", "serde::*", "serde::*", "tokio::*", "url::*", ] [features] default = [ "macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2", "unicode", "compat", ] # Brotli algorithm content-encoding support compress-brotli = ["actix-http/compress-brotli", "__compress"] # Gzip and deflate algorithms content-encoding support compress-gzip = ["actix-http/compress-gzip", "__compress"] # Zstd algorithm content-encoding support compress-zstd = ["actix-http/compress-zstd", "__compress"] # Routing and runtime proc macros macros = ["dep:actix-macros", "dep:actix-web-codegen"] # Cookies support cookies = ["dep:cookie"] # Secure & signed cookies secure-cookies = ["cookies", "cookie/secure"] # HTTP/2 support (including h2c). http2 = ["actix-http/http2"] # TLS via OpenSSL openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # TLS via Rustls v0.20 rustls = ["rustls-0_20"] # TLS via Rustls v0.20 rustls-0_20 = ["__tls", "http2", "actix-http/rustls-0_20", "actix-tls/accept", "actix-tls/rustls-0_20"] # TLS via Rustls v0.21 rustls-0_21 = ["__tls", "http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls/rustls-0_21"] # TLS via Rustls v0.22 rustls-0_22 = ["__tls", "http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"] # TLS via Rustls v0.23 rustls-0_23 = ["__tls", "http2", "actix-http/rustls-0_23", "actix-tls/accept", "actix-tls/rustls-0_23"] # Full unicode support unicode = ["dep:regex", "actix-router/unicode"] # Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] # Internal (PRIVATE!) features used to aid checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __tls = [] # io-uring feature only available for Linux OSes. experimental-io-uring = ["actix-server/io-uring"] # Feature group which, when disabled, helps migrate code to v5.0. compat = [ "compat-routing-macros-force-pub", ] # Opt-out forwards-compatibility for handler visibility inheritance fix. compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"] [dependencies] actix-codec = "0.5" actix-macros = { version = "0.2.3", optional = true } actix-rt = { version = "2.6", default-features = false } actix-server = "2" actix-service = "2" actix-utils = "3" actix-tls = { version = "3.4", default-features = false, optional = true } actix-http = { version = "3.7", features = ["ws"] } actix-router = { version = "0.5.3", default-features = false, features = ["http"] } actix-web-codegen = { version = "4.3", optional = true, default-features = false } ahash = "0.8" bytes = "1" bytestring = "1" cfg-if = "1" cookie = { version = "0.16", features = ["percent-encode"], optional = true } derive_more = "0.99.8" encoding_rs = "0.8" futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } itoa = "1" impl-more = "0.1.4" language-tags = "0.3" log = "0.4" mime = "0.3" once_cell = "1.5" pin-project-lite = "0.2.7" regex = { version = "1.5.5", optional = true } regex-lite = "0.1" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" smallvec = "1.6.1" socket2 = "0.5" time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] actix-files = "0.6" actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } awc = { version = "3", features = ["openssl"] } brotli = "6" const-str = "0.5" core_affinity = "0.8" criterion = { version = "0.5", features = ["html_reports"] } env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } rand = "0.8" rcgen = "0.13" rustls-pemfile = "2" serde = { version = "1.0", features = ["derive"] } static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } tls-rustls = { package = "rustls", version = "0.23" } tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } zstd = "0.13" [lints] workspace = true [[test]] name = "test_server" required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [[test]] name = "compression" required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] [[example]] name = "basic" required-features = ["compress-gzip"] [[example]] name = "uds" required-features = ["compress-gzip"] [[example]] name = "on-connect" required-features = [] [[bench]] name = "server" harness = false [[bench]] name = "service" harness = false [[bench]] name = "responder" harness = false actix-web-4.9.0/LICENSE-APACHE000064400000000000000000000261201046102023000134460ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. actix-web-4.9.0/LICENSE-MIT000064400000000000000000000020421046102023000131530ustar 00000000000000Copyright (c) 2017-NOW Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. actix-web-4.9.0/MIGRATION-0.x.md000064400000000000000000000125331046102023000140030ustar 00000000000000# 0.7.15 - The `' '` character is not percent decoded anymore before matching routes. If you need to use it in your routes, you should use `%20`. instead of ```rust fn main() { let app = App::new().resource("/my index", |r| { r.method(http::Method::GET) .with(index); }); } ``` use ```rust fn main() { let app = App::new().resource("/my%20index", |r| { r.method(http::Method::GET) .with(index); }); } ``` - If you used `AsyncResult::async` you need to replace it with `AsyncResult::future` # 0.7.4 - `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple even for handler with one parameter. # 0.7 - `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method. instead of ```rust fn index(req: HttpRequest) -> impl Responder { req .from_err() .fold(...) .... } ``` use `.payload()` ```rust fn index(req: HttpRequest) -> impl Responder { req .payload() // <- get request payload stream .from_err() .fold(...) .... } ``` - [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) trait uses `&HttpRequest` instead of `&mut HttpRequest`. - Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. instead of ```rust fn index(query: Query<..>, info: Json impl Responder {} ``` use tuple of extractors and use `.with()` for registration: ```rust fn index((query, json): (Query<..>, Json impl Responder {} ``` - `Handler::handle()` uses `&self` instead of `&mut self` - `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value - Removed deprecated `HttpServer::threads()`, use [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. - Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver` - `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()` instead of ```rust fn main() { let app = App::new().resource("/index.html", |r| { r.method(http::Method::GET) .with(index) .limit(4096); // <- limit size of the payload }); } ``` use ```rust fn main() { let app = App::new().resource("/index.html", |r| { r.method(http::Method::GET) .with_config(index, |cfg| { // <- register handler cfg.limit(4096); // <- limit size of the payload }) }); } ``` - `Route::with_async()` does not return `ExtractorConfig`, to configure extractor use `Route::with_async_config()` # 0.6 - `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` - `ws::Message::Close` now includes optional close reason. `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. - `HttpServer::threads()` renamed to `HttpServer::workers()`. - `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. - `HttpRequest::extensions()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference. - Instead of `use actix_web::middleware::{ CookieSessionBackend, CookieSessionError, RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` use `actix_web::middleware::session` `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` - `FromRequest::from_request()` accepts mutable reference to a request - `FromRequest::Result` has to implement `Into>` - [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) is generic over `S` - Use `Query` extractor instead of HttpRequest::query()`. ```rust fn index(q: Query>) -> Result<..> { ... } ``` or ```rust let q = Query::>::extract(req); ``` - Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter` # 0.5 - `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result` - `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module - `actix_web::header` moved to `actix_web::http::header` - `NormalizePath` moved to `actix_web::http` module - `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, shortcut for `actix_web::server::HttpServer::new()` - `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself - `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead. - `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type - `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` functions should be used instead - `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>` - `Application` renamed to a `App` - `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` actix-web-4.9.0/MIGRATION-1.0.md000064400000000000000000000171001046102023000136670ustar 00000000000000## 1.0.1 - Cors middleware has been moved to `actix-cors` crate instead of ```rust use actix_web::middleware::cors::Cors; ``` use ```rust use actix_cors::Cors; ``` - Identity middleware has been moved to `actix-identity` crate instead of ```rust use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService}; ``` use ```rust use actix_identity::{Identity, CookieIdentityPolicy, IdentityService}; ``` ## 1.0.0 - Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration instead of ```rust #[derive(Default)] struct ExtractorConfig { config: String, } impl FromRequest for YourExtractor { type Config = ExtractorConfig; type Result = Result; fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { println!("use the config: {:?}", cfg.config); ... } } App::new().resource("/route_with_config", |r| { r.post().with_config(handler_fn, |cfg| { cfg.0.config = "test".to_string(); }) }) ``` use the HttpRequest to get the configuration like any other `Data` with `req.app_data::()` and set it with the `data()` method on the `resource` ```rust #[derive(Default)] struct ExtractorConfig { config: String, } impl FromRequest for YourExtractor { type Error = Error; type Future = Result; type Config = ExtractorConfig; fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let cfg = req.app_data::(); println!("config data?: {:?}", cfg.unwrap().role); ... } } App::new().service( resource("/route_with_config") .data(ExtractorConfig { config: "test".to_string(), }) .route(post().to(handler_fn)), ) ``` - Resource registration. 1.0 version uses generalized resource registration via `.service()` method. instead of ```rust App.new().resource("/welcome", |r| r.f(welcome)) ``` use App's or Scope's `.service()` method. `.service()` method accepts object that implements `HttpServiceFactory` trait. By default actix-web provides `Resource` and `Scope` services. ```rust App.new().service( web::resource("/welcome") .route(web::get().to(welcome)) .route(web::post().to(post_handler)) ``` - Scope registration. instead of ```rust let app = App::new().scope("/{project_id}", |scope| { scope .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) }); ``` use `.service()` for registration and `web::scope()` as scope object factory. ```rust let app = App::new().service( web::scope("/{project_id}") .service(web::resource("/path1").to(|| HttpResponse::Ok())) .service(web::resource("/path2").to(|| HttpResponse::Ok())) .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) ); ``` - `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. instead of ```rust App.new().resource("/welcome", |r| r.with(welcome)) ``` use `.to()` or `.to_async()` methods ```rust App.new().service(web::resource("/welcome").to(welcome)) ``` - Passing arguments to handler with extractors, multiple arguments are allowed instead of ```rust fn welcome((body, req): (Bytes, HttpRequest)) -> ... { ... } ``` use multiple arguments ```rust fn welcome(body: Bytes, req: HttpRequest) -> ... { ... } ``` - `.f()`, `.a()` and `.h()` handler registration methods have been removed. Use `.to()` for handlers and `.to_async()` for async handlers. Handler function must use extractors. instead of ```rust App.new().resource("/welcome", |r| r.f(welcome)) ``` use App's `to()` or `to_async()` methods ```rust App.new().service(web::resource("/welcome").to(welcome)) ``` - `HttpRequest` does not provide access to request's payload stream. instead of ```rust fn index(req: &HttpRequest) -> Box> { req .payload() .from_err() .fold((), |_, chunk| { ... }) .map(|_| HttpResponse::Ok().finish()) .responder() } ``` use `Payload` extractor ```rust fn index(stream: web::Payload) -> impl Future { stream .from_err() .fold((), |_, chunk| { ... }) .map(|_| HttpResponse::Ok().finish()) } ``` - `State` is now `Data`. You register Data during the App initialization process and then access it from handlers either using a Data extractor or using HttpRequest's api. instead of ```rust App.with_state(T) ``` use App's `data` method ```rust App.new() .data(T) ``` and either use the Data extractor within your handler ```rust use actix_web::web::Data; fn endpoint_handler(Data)){ ... } ``` .. or access your Data element from the HttpRequest ```rust fn endpoint_handler(req: HttpRequest) { let data: Option> = req.app_data::(); } ``` - AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. instead of ```rust use actix_web::AsyncResponder; fn endpoint_handler(...) -> impl Future{ ... .responder() } ``` .. simply omit AsyncResponder and the corresponding responder() finish method - Middleware instead of ```rust let app = App::new() .middleware(middleware::Logger::default()) ``` use `.wrap()` method ```rust let app = App::new() .wrap(middleware::Logger::default()) .route("/index.html", web::get().to(index)); ``` - `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. instead of ```rust fn index(req: &HttpRequest) -> Responder { req.body() .and_then(|body| { ... }) } ``` use ```rust fn index(body: Bytes) -> Responder { ... } ``` - `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type - StaticFiles and NamedFile have been moved to a separate crate. instead of `use actix_web::fs::StaticFile` use `use actix_files::Files` instead of `use actix_web::fs::Namedfile` use `use actix_files::NamedFile` - Multipart has been moved to a separate crate. instead of `use actix_web::multipart::Multipart` use `use actix_multipart::Multipart` - Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. - Session middleware moved to actix-session crate - Actors support have been moved to `actix-web-actors` crate - Custom Error Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller. Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError: ```rust fn render_response(&self) -> HttpResponse { self.error_response() } ``` actix-web-4.9.0/MIGRATION-2.0.md000064400000000000000000000024641046102023000136770ustar 00000000000000# Migrating to 2.0.0 - `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to `.await` on `run` method result, in that case it awaits server exit. - `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. Stored data is available via `HttpRequest::app_data()` method at runtime. - Extractor configuration must be registered with `App::app_data()` instead of `App::data()` - Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` replace `fn` with `async fn` to convert sync handler to async - `actix_http_test::TestServer` moved to `actix_web::test` module. To start test server use `test::start()` or `test_start_with_config()` methods - `ResponseError` trait has been refactored. `ResponseError::error_response()` renders http response. - Feature `rust-tls` renamed to `rustls` instead of ```rust actix-web = { version = "2.0.0", features = ["rust-tls"] } ``` use ```rust actix-web = { version = "2.0.0", features = ["rustls"] } ``` - Feature `ssl` renamed to `openssl` instead of ```rust actix-web = { version = "2.0.0", features = ["ssl"] } ``` use ```rust actix-web = { version = "2.0.0", features = ["openssl"] } ``` - `Cors` builder now requires that you call `.finish()` to construct the middleware actix-web-4.9.0/MIGRATION-3.0.md000064400000000000000000000046211046102023000136750ustar 00000000000000# Migrating to 3.0.0 - The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. - Cookie handling has been offloaded to the `cookie` crate: - `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. - Some types now require lifetime parameters. - The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects any `actix-web` method previously expecting a time v0.1 input. - Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. - actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors` - content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function. - `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. - Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use destructuring or `.into_inner()`. For example: ```rust // Previously: async fn some_route(path: web::Path<(String, String)>) -> String { format!("Hello, {} {}", path.0, path.1) } // Now (this also worked before): async fn some_route(path: web::Path<(String, String)>) -> String { let (first_name, last_name) = path.into_inner(); format!("Hello, {} {}", first_name, last_name) } // Or (this wasn't previously supported): async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String { format!("Hello, {} {}", first_name, last_name) } ``` - `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. - `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. actix-web-4.9.0/MIGRATION-4.0.md000064400000000000000000000535721046102023000137070ustar 00000000000000# Migrating to 4.0.0 This guide walks you through the process of migrating from v3.x.y to v4.x.y. If you are migrating to v4.x.y from an older version of Actix Web (v2.x.y or earlier), check out the other historical migration notes in this folder. This document is not designed to be exhaustive—it focuses on the most significant changes in v4. You can find an exhaustive changelog in the changelogs for [`actix-web`](./CHANGES.md#400---2022-02-25) and [`actix-http`](../actix-http/CHANGES.md#300---2022-02-25), complete with PR links. If you think there are any changes that deserve to be called out in this document, please open an issue or pull request. Headings marked with :warning: are **breaking behavioral changes**. They will probably not surface as compile-time errors though automated tests _might_ detect their effects on your app. ## Table of Contents: - [MSRV](#msrv) - [Tokio v1 Ecosystem](#tokio-v1-ecosystem) - [Module Structure](#module-structure) - [`NormalizePath` Middleware :warning:](#normalizepath-middleware-warning) - [Server Settings :warning:](#server-settings-warning) - [`FromRequest` Trait](#fromrequest-trait) - [Compression Feature Flags](#compression-feature-flags) - [`web::Path`](#webpath) - [Rustls Crate Upgrade](#rustls-crate-upgrade) - [Removed `awc` Client Re-export](#removed-awc-client-re-export) - [Integration Testing Utils Moved To `actix-test`](#integration-testing-utils-moved-to-actix-test) - [Header APIs](#header-apis) - [Response Body Types](#response-body-types) - [Middleware Trait APIs](#middleware-trait-apis) - [`Responder` Trait](#responder-trait) - [`App::data` Deprecation :warning:](#appdata-deprecation-warning) - [Direct Dependency On `actix-rt` And `actix-service`](#direct-dependency-on-actix-rt-and-actix-service) - [Server Must Be Polled :warning:](#server-must-be-polled-warning) - [Guards API](#guards-api) - [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously) - [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain) - [`web::block`](#webblock) ## MSRV The MSRV of Actix Web has been raised from 1.42 to 1.54. ## Tokio v1 Ecosystem Actix Web v4 is now underpinned by `tokio`'s v1 ecosystem. `cargo` supports having multiple versions of the same crate within the same dependency tree, but `tokio` v1 does not interoperate transparently with its previous versions (v0.2, v0.1). Some of your dependencies might rely on `tokio`, either directly or indirectly—if they are using an older version of `tokio`, check if an update is available. The following command can help you to identify these dependencies: ```sh # Find all crates in your dependency tree that depend on `tokio` # It also reports the different versions of `tokio` in your dependency tree. cargo tree -i tokio # if you depend on multiple versions of tokio, use this command to # list the dependencies relying on a specific version of tokio: cargo tree -i tokio:0.2.25 ``` ## Module Structure Lots of modules have been re-organized in this release. If a compile error refers to "item XYZ not found in module..." or "module XYZ not found", check the [documentation on docs.rs](https://docs.rs/actix-web) to search for items' new locations. ## `NormalizePath` Middleware :warning: The default `NormalizePath` behavior now strips trailing slashes by default. This was the _documented_ behaviour in Actix Web v3, but the _actual_ behaviour differed. The discrepancy has now been resolved. As a consequence of this change, routes defined with trailing slashes will become inaccessible when using `NormalizePath::default()`. Calling `NormalizePath::default()` will log a warning. We suggest to use `new` or `trim`. ```diff - #[get("/test/")] + #[get("/test")] async fn handler() { App::new() - .wrap(NormalizePath::default()) + .wrap(NormalizePath::trim()) ``` Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. ## Server Settings :warning: Until Actix Web v4, the underlying `actix-server` crate used the number of available **logical** cores as the default number of worker threads. The new default is the number of [physical CPU cores available](https://github.com/actix/actix-net/commit/3a3d654c). For more information about this change, refer to [this analysis](https://github.com/actix/actix-web/issues/957). If you notice performance regressions, please open a new issue detailing your observations. ## `FromRequest` Trait The associated type `Config` of `FromRequest` was removed. If you have custom extractors, you can just remove this implementation and refer to config types directly, if required. ```diff impl FromRequest for MyExtractor { - type Config = (); } ``` Consequently, the `FromRequest::configure` method was also removed. Config for extractors is still provided using `App::app_data` but should now be constructed in a standalone way. ## Compression Feature Flags The `compress` feature flag has been split into more granular feature flags, one for each supported algorithm (brotli, gzip, zstd). By default, all compression algorithms are enabled. If you want to select specific compression codecs, the new flags are: - `compress-brotli` - `compress-gzip` - `compress-zstd` ## `web::Path` The inner field for `web::Path` is now private. It was causing ambiguity when trying to use tuple indexing due to its `Deref` implementation. ```diff - async fn handler(web::Path((foo, bar)): web::Path<(String, String)>) { + async fn handler(params: web::Path<(String, String)>) { + let (foo, bar) = params.into_inner(); ``` An alternative [path param type with public field but no `Deref` impl is available in `actix-web-lab`](https://docs.rs/actix-web-lab/0.12.0/actix_web_lab/extract/struct.Path.html). ## Rustls Crate Upgrade Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/master/https-tls/rustls/) ## Removed `awc` Client Re-export Actix Web's sister crate `awc` is no longer re-exported through the `client` module. This allows `awc` to have its own release cadence—its breaking changes are no longer blocked by Actix Web's (more conservative) release schedule. ```diff - use actix_web::client::Client; + use awc::Client; ``` ## Integration Testing Utils Moved To `actix-test` `TestServer` has been moved to its own crate, [`actix-test`](https://docs.rs/actix-test). ```diff - use use actix_web::test::start; + use use actix_test::start; ``` `TestServer` previously lived in `actix_web::test`, but it depends on `awc` which is no longer part of Actix Web's public API (see above). ## Header APIs Header related APIs have been standardized across all `actix-*` crates. The terminology now better matches the underlying `HeaderMap` naming conventions. In short, "insert" always indicates that any existing headers with the same name are overridden, while "append" is used for adding with no removal (e.g. multi-valued headers). For request and response builder APIs, the new methods provide a unified interface for adding key-value pairs _and_ typed headers, which can often be more expressive. ```diff - .set_header("Api-Key", "1234") + .insert_header(("Api-Key", "1234")) - .header("Api-Key", "1234") + .append_header(("Api-Key", "1234")) - .set(ContentType::json()) + .insert_header(ContentType::json()) ``` We chose to deprecate most of the old methods instead of removing them immediately—the warning notes will guide you on how to update. ## Response Body Types There have been a lot of changes to response body types. They are now more expressive and their purpose should be more intuitive. We have boosted the quality and completeness of the documentation for all items in the [`body` module](https://docs.rs/actix-web/4/actix_web/body). ### `ResponseBody` `ResponseBody` is gone. Its purpose was confusing and has been replaced by better components. ### `Body` `Body` is also gone. In combination with `ResponseBody`, the API it provided was sub-optimal and did not encourage expressive types. Here are the equivalents in the new system (check docs): - `Body::None` => `body::None::new()` - `Body::Empty` => `()` / `web::Bytes::new()` - `Body::Bytes` => `web::Bytes::from(...)` - `Body::Message` => `.boxed()` / `BoxBody` ### `BoxBody` `BoxBody` is a new type-erased body type. It can be useful when writing handlers, responders, and middleware when you want to trade a (very) small amount of performance for a simpler type. Creating a boxed body is done most efficiently by calling [`.boxed()`](https://docs.rs/actix-web/4/actix_web/body/trait.MessageBody.html#method.boxed) on a `MessageBody` type. ### `EitherBody` `EitherBody` is a new "either" type that implements `MessageBody` It is particularly useful in middleware that can bail early, returning their own response plus body type. By default the "right" variant is `BoxBody` (i.e., `EitherBody` === `EitherBody`) but it can be anything that implements `MessageBody`. For example, it will be common among middleware which value performance of the hot path to use: ```rust type Response = Result>, Error> ``` This can be read (ignoring the `Result`) as "resolves with a `ServiceResponse` that is either the inner service's `B` body type or a boxed body type from elsewhere, likely constructed within the middleware itself". Of course, if your middleware contains only simple string other/error responses, it's possible to use them without boxes at the cost of a less simple implementation: ```rust type Response = Result>, Error> ``` ### Error Handlers `ErrorHandlers` is a commonly used middleware that has changed in design slightly due to the other body type changes. In particular, an implicit `EitherBody` is used in the `ErrorHandlerResponse` type. An `ErrorHandlerResponse` now expects a `ServiceResponse>` to be returned within response variants. The following is a migration for an error handler that **only modifies** the response argument (left body). ```diff fn add_error_header(mut res: ServiceResponse) -> Result, Error> { res.response_mut().headers_mut().insert( header::CONTENT_TYPE, header::HeaderValue::from_static("Error"), ); - Ok(ErrorHandlerResponse::Response(res)) + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } ``` The following is a migration for an error handler that creates a new response instead (right body). ```diff fn error_handler(res: ServiceResponse) -> Result, Error> { - let req = res.request().clone(); + let (req, _res) = res.into_parts(); let res = actix_files::NamedFile::open("./templates/404.html")? .set_status_code(StatusCode::NOT_FOUND) - .into_response(&req)? - .into_body(); + .into_response(&req); - let res = ServiceResponse::new(req, res); + let res = ServiceResponse::new(req, res).map_into_right_body(); Ok(ErrorHandlerResponse::Response(res)) } ``` ## Middleware Trait APIs The underlying traits that are used for creating middleware, `Service`, `ServiceFactory`, and `Transform`, have changed in design. - The associated `Request` type has moved to the type parameter position in order to allow multiple request implementations in other areas of the service stack. - The `self` arguments in `Service` have changed from exclusive (mutable) borrows to shared (immutable) borrows. Since most service layers, such as middleware, do not host mutable state, it reduces the runtime overhead in places where a `RefCell` used to be required for wrapping an inner service. - We've also introduced some macros that reduce boilerplate when implementing `poll_ready`. - Further to the guidance on [response body types](#response-body-types), any use of the old methods on `ServiceResponse` designed to match up body types (e.g., the old `into_body` method), should be replaced with an explicit response body type utilizing `EitherBody`. A typical migration would look like this: ```diff use std::{ - cell::RefCell, future::Future, pin::Pin, rc::Rc, - task::{Context, Poll}, }; use actix_web::{ dev::{Service, ServiceRequest, ServiceResponse, Transform}, Error, }; use futures_util::future::{ok, LocalBoxFuture, Ready}; pub struct SayHi; - impl Transform for SayHi + impl Transform for SayHi where - S: Service, Error = Error>, + S: Service, Error = Error>, S::Future: 'static, B: 'static, { - type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); type Transform = SayHiMiddleware; type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(SayHiMiddleware { - service: Rc::new(RefCell::new(service)), + service: Rc::new(service), }) } } pub struct SayHiMiddleware { - service: Rc>, + service: Rc, } - impl Service for SayHiMiddleware + impl Service for SayHiMiddleware where - S: Service, Error = Error>, + S: Service, Error = Error>, S::Future: 'static, B: 'static, { - type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_web::dev::forward_ready!(service); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { println!("Hi from start. You requested: {}", req.path()); let fut = self.service.call(req); Box::pin(async move { let res = fut.await?; println!("Hi from response"); Ok(res) }) } } ``` This new design is forward-looking and should ease transition to traits that support the upcoming Generic Associated Type (GAT) feature in Rust while also trimming down the boilerplate required to implement middleware. We understand that creating middleware is still a pain point for Actix Web and we hope to provide [an even more ergonomic solution](https://docs.rs/actix-web-lab/0.11.0/actix_web_lab/middleware/fn.from_fn.html) in a v4.x release. ## `Responder` Trait The `Responder` trait's interface has changed. Errors should be handled and converted to responses within the `respond_to` method. It's also no longer async so the associated `type Future` has been removed; there was no compelling use case found for it. These changes simplify the interface and implementation a lot. Now that more emphasis is placed on expressive body types, as explained in the [body types migration section](#response-body-types), this trait has introduced an associated `type Body`. The simplest migration will be to use `BoxBody` + `.map_into_boxed_body()` but if there is a more expressive type for your responder then try to use that instead. ```diff impl Responder for &'static str { - type Error = Error; - type Future = Ready>; + type Body = &'static str; - fn respond_to(self, req: &HttpRequest) -> Self::Future { + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let res = HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self); - ok(res) + res } } ``` ## `App::data` Deprecation :warning: The `App::data` method is deprecated. Replace instances of this with `App::app_data`. Exposing both methods led to lots of confusion when trying to extract the data in handlers. Now, when using the `Data` wrapper, the type you put in to `app_data` is the same type you extract in handler arguments. You may need to review the [guidance on shared mutable state](https://docs.rs/actix-web/4/actix_web/struct.App.html#shared-mutable-state) in order to migrate this correctly. ```diff use actix_web::web::Data; #[get("/")] async fn handler(my_state: Data) -> { todo!() } HttpServer::new(|| { - App::new() - .data(MyState::default()) - .service(handler) + let my_state: Data = Data::new(MyState::default()); + + App::new() + .app_data(my_state) + .service(handler) }) ``` ## Direct Dependency On `actix-rt` And `actix-service` Improvements to module management and re-exports have resulted in not needing direct dependencies on these underlying crates for the vast majority of cases. In particular: - all traits necessary for creating middlewares are now re-exported through the `dev` modules; - `#[actix_web::test]` now exists for async test definitions. Relying on these re-exports will ease the transition to future versions of Actix Web. ```diff - use actix_service::{Service, Transform}; + use actix_web::dev::{Service, Transform}; ``` ```diff - #[actix_rt::test] + #[actix_web::test] async fn test_thing() { ``` ## Server Must Be Polled :warning: In order to _start_ serving requests, the `Server` object returned from `run` **must** be `poll`ed, `await`ed, or `spawn`ed. This was done to prevent unexpected behavior and ensure that things like signal handlers are able to function correctly when enabled. For example, in this contrived example where the server is started and then the main thread is sent to sleep, the server will no longer be able to serve requests with v4.0: ```rust #[actix_web::main] async fn main() { HttpServer::new(|| App::new().default_service(web::to(HttpResponse::Conflict))) .bind(("127.0.0.1", 8080)) .unwrap() .run(); thread::sleep(Duration::from_secs(1000)); } ``` ## Guards API Implementors of routing guards will need to use the modified interface of the `Guard` trait. The API is more flexible than before. See [guard module docs](https://docs.rs/actix-web/4/actix_web/guard/struct.GuardContext.html) for more details. ```diff struct MethodGuard(HttpMethod); impl Guard for MethodGuard { - fn check(&self, request: &RequestHead) -> bool { + fn check(&self, ctx: &GuardContext<'_>) -> bool { - request.method == self.0 + ctx.head().method == self.0 } } ``` ## Returning `HttpResponse` synchronously The implementation of `Future` for `HttpResponse` was removed because it was largely useless for all but the simplest handlers like `web::to(|| HttpResponse::Ok().finish())`. It also caused false positives on the `async_yields_async` clippy lint in reasonable scenarios. The compiler errors will looks something like: ``` web::to(|| HttpResponse::Ok().finish()) ^^^^^^^ the trait `Handler<_>` is not implemented for `[closure@...]` ``` This form should be replaced with explicit async functions and closures: ```diff - fn handler() -> HttpResponse { + async fn handler() -> HttpResponse { HttpResponse::Ok().finish() } ``` ```diff - web::to(|| HttpResponse::Ok().finish()) + web::to(|| async { HttpResponse::Ok().finish() }) ``` Or, for these extremely simple cases, utilise an `HttpResponseBuilder`: ```diff - web::to(|| HttpResponse::Ok().finish()) + web::to(HttpResponse::Ok) ``` ## `#[actix_web::main]` and `#[tokio::main]` Actix Web now works seamlessly with the primary way of starting a multi-threaded Tokio runtime, `#[tokio::main]`. Therefore, it is no longer necessary to spawn a thread when you need to run something alongside Actix Web that uses Tokio's multi-threaded mode; you can simply await the server within this context or, if preferred, use `tokio::spawn` just like any other async task. For now, `actix` actor support (and therefore WebSocket support via `actix-web-actors`) still requires `#[actix_web::main]` so that a `System` context is created. Designs are being created for an alternative WebSocket interface that does not require actors that should land sometime in the v4.x cycle. ## `web::block` The `web::block` helper has changed return type from roughly `async fn(fn() -> Result) Result>` to `async fn(fn() -> T) Result`. That's to say that the blocking function can now return things that are not `Result`s and it does not wrap error types anymore. If you still need to return `Result`s then you'll likely want to use double `?` after the `.await`. ```diff - let n: u32 = web::block(|| Ok(123)).await?; + let n: u32 = web::block(|| 123).await?; - let n: u32 = web::block(|| Ok(123)).await?; + let n: u32 = web::block(|| Ok(123)).await??; ``` ## `HttpResponse` as a `ResponseError` The implementation of `ResponseError` for `HttpResponse` has been removed. It was common in v3 to use `HttpResponse` as an error type in fallible handlers. The problem is that `HttpResponse` contains no knowledge or reference to the source error. Being able to guarantee that an "error" response actually contains an error reference makes middleware and other parts of Actix Web more effective. The error response builders in the `error` module were available in v3 but are now the best method for simple error responses without requiring you to implement the trait on your own custom error types. These builders can receive simple strings and third party errors that can not implement the `ResponseError` trait. A few common patterns are affected by this change: ```diff - Err(HttpResponse::InternalServerError().finish()) + Err(error::ErrorInternalServerError("reason")) - Err(HttpResponse::InternalServerError().body(third_party_error.to_string())) + Err(error::ErrorInternalServerError(err)) - .map_err(|err| HttpResponse::InternalServerError().finish())? + .map_err(error::ErrorInternalServerError)? ``` actix-web-4.9.0/README.md000064400000000000000000000107311046102023000130020ustar 00000000000000

Actix Web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust

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

## Features - Supports _HTTP/1.x_ and _HTTP/2_ - Streaming and pipelining - Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros - Full [Tokio](https://tokio.rs) compatibility - Keep-alive and slow requests handling - Client/server [WebSockets](https://actix.rs/docs/websockets/) support - Transparent content compression/decompression (br, gzip, deflate, zstd) - Multipart streams - Static assets - SSL support using OpenSSL or Rustls - Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) - Integrates with the [`awc` HTTP client](https://docs.rs/awc/) - Runs on stable Rust 1.72+ ## Documentation - [Website & User Guide](https://actix.rs) - [Examples Repository](https://github.com/actix/examples) - [API Documentation](https://docs.rs/actix-web) - [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) ## Example Dependencies: ```toml [dependencies] actix-web = "4" ``` Code: ```rust use actix_web::{get, web, App, HttpServer, Responder}; #[get("/hello/{name}")] async fn greet(name: web::Path) -> impl Responder { format!("Hello {name}!") } #[actix_web::main] // or #[tokio::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(greet) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` ### More Examples - [Hello World](https://github.com/actix/examples/tree/master/basics/hello-world) - [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics) - [Application State](https://github.com/actix/examples/tree/master/basics/state) - [JSON Handling](https://github.com/actix/examples/tree/master/json/json) - [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart) - [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb) - [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel) - [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite) - [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres) - [Tera Templates](https://github.com/actix/examples/tree/master/templating/tera) - [Askama Templates](https://github.com/actix/examples/tree/master/templating/askama) - [HTTPS using Rustls](https://github.com/actix/examples/tree/master/https-tls/rustls) - [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/https-tls/openssl) - [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets) - [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat) You may consider checking out [this directory](https://github.com/actix/examples/tree/master) for more examples. ## Benchmarks One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r21&test=composite). ## License This project is licensed under either of the following licenses, at your option: - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0]) - MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT]) ## Code of Conduct Contribution to the `actix/actix-web` repo is organized under the terms of the Contributor Covenant. The Actix team promises to intervene to uphold that code of conduct. actix-web-4.9.0/benches/responder.rs000064400000000000000000000050371046102023000155040ustar 00000000000000use std::{future::Future, time::Instant}; use actix_http::body::BoxBody; use actix_utils::future::{ready, Ready}; use actix_web::{http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder}; use criterion::{criterion_group, criterion_main, Criterion}; use futures_util::future::join_all; // responder simulate the old responder trait. trait FutureResponder { type Error; type Future: Future>; fn future_respond_to(self, req: &HttpRequest) -> Self::Future; } // a simple wrapper type around string struct StringResponder(String); impl FutureResponder for StringResponder { type Error = Error; type Future = Ready>; fn future_respond_to(self, _: &HttpRequest) -> Self::Future { // this is default builder for string response in both new and old responder trait. ready(Ok(HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0))) } } impl Responder for StringResponder { type Body = BoxBody; fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) } } fn future_responder(c: &mut Criterion) { let rt = actix_rt::System::new(); let req = TestRequest::default().to_http_request(); c.bench_function("future_responder", move |b| { b.iter_custom(|_| { let futs = (0..100_000).map(|_| async { StringResponder(String::from("Hello World!!")) .future_respond_to(&req) .await }); let futs = join_all(futs); let start = Instant::now(); let _res = rt.block_on(futs); start.elapsed() }) }); } fn responder(c: &mut Criterion) { let rt = actix_rt::System::new(); let req = TestRequest::default().to_http_request(); c.bench_function("responder", move |b| { b.iter_custom(|_| { let responders = (0..100_000).map(|_| StringResponder(String::from("Hello World!!"))); let start = Instant::now(); let _res = rt.block_on(async { // don't need runtime block on but to be fair. responders.map(|r| r.respond_to(&req)).collect::>() }); start.elapsed() }) }); } criterion_group!(responder_bench, future_responder, responder); criterion_main!(responder_bench); actix-web-4.9.0/benches/server.rs000064400000000000000000000062461046102023000150140ustar 00000000000000use actix_web::{web, App, HttpResponse}; use awc::Client; use criterion::{criterion_group, criterion_main, Criterion}; use futures_util::future::join_all; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; // benchmark sending all requests at the same time fn bench_async_burst(c: &mut Criterion) { // We are using System here, since Runtime requires preinitialized tokio // Maybe add to actix_rt docs let rt = actix_rt::System::new(); let srv = rt.block_on(async { actix_test::start(|| { App::new().service( web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), ) }) }); let url = srv.url("/"); c.bench_function("get_body_async_burst", move |b| { b.iter_custom(|iters| { rt.block_on(async { let client = Client::new().get(url.clone()).freeze().unwrap(); let start = std::time::Instant::now(); // benchmark body let burst = (0..iters).map(|_| client.send()); let resps = join_all(burst).await; let elapsed = start.elapsed(); // if there are failed requests that might be an issue let failed = resps.iter().filter(|r| r.is_err()).count(); if failed > 0 { eprintln!("failed {} requests (might be bench timeout)", failed); }; elapsed }) }) }); } criterion_group!(server_benches, bench_async_burst); criterion_main!(server_benches); actix-web-4.9.0/benches/service.rs000064400000000000000000000071161046102023000151430ustar 00000000000000use std::{cell::RefCell, rc::Rc}; use actix_service::Service; use actix_web::{ dev::{ServiceRequest, ServiceResponse}, test::{init_service, ok_service, TestRequest}, web, App, Error, HttpResponse, }; use criterion::{criterion_main, Criterion}; /// Criterion Benchmark for async Service /// Should be used from within criterion group: /// ```ignore /// let mut criterion: ::criterion::Criterion<_> = /// ::criterion::Criterion::default().configure_from_args(); /// bench_async_service(&mut criterion, ok_service(), "async_service_direct"); /// ``` /// /// Usable for benching Service wrappers: /// Using minimum service code implementation we first measure /// time to run minimum service, then measure time with wrapper. /// /// Sample output /// async_service_direct time: [1.0908 us 1.1656 us 1.2613 us] pub fn bench_async_service(c: &mut Criterion, srv: S, name: &str) where S: Service + 'static, { let rt = actix_rt::System::new(); let srv = Rc::new(RefCell::new(srv)); let req = TestRequest::default().to_srv_request(); assert!(rt .block_on(srv.borrow_mut().call(req)) .unwrap() .status() .is_success()); // start benchmark loops c.bench_function(name, move |b| { b.iter_custom(|iters| { let srv = srv.clone(); // exclude request generation, it appears it takes significant time vs call (3us vs 1us) let futs = (0..iters) .map(|_| TestRequest::default().to_srv_request()) .map(|req| srv.borrow_mut().call(req)); let start = std::time::Instant::now(); // benchmark body rt.block_on(async move { for fut in futs { fut.await.unwrap(); } }); // check that at least first request succeeded start.elapsed() }) }); } async fn index(req: ServiceRequest) -> Result { Ok(req.into_response(HttpResponse::Ok().finish())) } // Benchmark basic WebService directly // this approach is usable for benching WebService, though it adds some time to direct service call: // Sample results on MacBook Pro '14 // time: [2.0724 us 2.1345 us 2.2074 us] fn async_web_service(c: &mut Criterion) { let rt = actix_rt::System::new(); let srv = Rc::new(RefCell::new(rt.block_on(init_service( App::new().service(web::service("/").finish(index)), )))); let req = TestRequest::get().uri("/").to_request(); assert!(rt .block_on(srv.borrow_mut().call(req)) .unwrap() .status() .is_success()); // start benchmark loops c.bench_function("async_web_service_direct", move |b| { b.iter_custom(|iters| { let srv = srv.clone(); let futs = (0..iters) .map(|_| TestRequest::get().uri("/").to_request()) .map(|req| srv.borrow_mut().call(req)); let start = std::time::Instant::now(); // benchmark body rt.block_on(async move { for fut in futs { fut.await.unwrap(); } }); // check that at least first request succeeded start.elapsed() }) }); } pub fn service_benches() { let mut criterion: ::criterion::Criterion<_> = ::criterion::Criterion::default().configure_from_args(); bench_async_service(&mut criterion, ok_service(), "async_service_direct"); async_web_service(&mut criterion); } criterion_main!(service_benches); actix-web-4.9.0/examples/README.md000064400000000000000000000002751046102023000146220ustar 00000000000000# Actix Web Examples This folder contain just a few standalone code samples. There is a much larger registry of example projects [in the examples repo](https://github.com/actix/examples). actix-web-4.9.0/examples/basic.rs000064400000000000000000000026531046102023000147740ustar 00000000000000use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] async fn index(req: HttpRequest, name: web::Path) -> String { println!("REQ: {:?}", req); format!("Hello: {}!\r\n", name) } async fn index_async(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); "Hello world!\r\n" } #[get("/")] async fn no_params() -> &'static str { "Hello world!\r\n" } #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); log::info!("starting HTTP server at http://localhost:8080"); HttpServer::new(|| { App::new() .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default().log_target("http_log")) .service(index) .service(no_params) .service( web::resource("/resource2/index.html") .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3"))) .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) .bind(("127.0.0.1", 8080))? .workers(1) .run() .await } actix-web-4.9.0/examples/macroless.rs000064400000000000000000000011241046102023000156730ustar 00000000000000use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; async fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); "Hello world!\r\n" } fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); rt::System::new().block_on( HttpServer::new(|| { App::new() .wrap(middleware::Logger::default()) .service(web::resource("/").route(web::get().to(index))) }) .bind(("127.0.0.1", 8080))? .workers(1) .run(), ) } actix-web-4.9.0/examples/middleware_from_fn.rs000064400000000000000000000071111046102023000175300ustar 00000000000000//! Shows a couple of ways to use the `from_fn` middleware. use std::{collections::HashMap, io, rc::Rc, time::Duration}; use actix_web::{ body::MessageBody, dev::{Service, ServiceRequest, ServiceResponse, Transform}, http::header::{self, HeaderValue, Range}, middleware::{from_fn, Logger, Next}, web::{self, Header, Query}, App, Error, HttpResponse, HttpServer, }; async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { next.call(req).await } async fn print_range_header( range_header: Option>, req: ServiceRequest, next: Next, ) -> Result, Error> { if let Some(Header(range)) = range_header { println!("Range: {range}"); } else { println!("No Range header"); } next.call(req).await } async fn mutate_body_type( req: ServiceRequest, next: Next, ) -> Result, Error> { let res = next.call(req).await?; Ok(res.map_into_left_body::<()>()) } async fn mutate_body_type_with_extractors( string_body: String, query: Query>, req: ServiceRequest, next: Next, ) -> Result, Error> { println!("body is: {string_body}"); println!("query string: {query:?}"); let res = next.call(req).await?; Ok(res.map_body(move |_, _| string_body)) } async fn timeout_10secs( req: ServiceRequest, next: Next, ) -> Result, Error> { match tokio::time::timeout(Duration::from_secs(10), next.call(req)).await { Ok(res) => res, Err(_err) => Err(actix_web::error::ErrorRequestTimeout("")), } } struct MyMw(bool); impl MyMw { async fn mw_cb( &self, req: ServiceRequest, next: Next, ) -> Result, Error> { let mut res = match self.0 { true => req.into_response("short-circuited").map_into_right_body(), false => next.call(req).await?.map_into_left_body(), }; res.headers_mut() .insert(header::WARNING, HeaderValue::from_static("42")); Ok(res) } pub fn into_middleware( self, ) -> impl Transform< S, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > where S: Service, Error = Error> + 'static, B: MessageBody + 'static, { let this = Rc::new(self); from_fn(move |req, next| { let this = Rc::clone(&this); async move { Self::mw_cb(&this, req, next).await } }) } } #[actix_web::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let bind = ("127.0.0.1", 8080); log::info!("staring server at http://{}:{}", &bind.0, &bind.1); HttpServer::new(|| { App::new() .wrap(from_fn(noop)) .wrap(from_fn(print_range_header)) .wrap(from_fn(mutate_body_type)) .wrap(from_fn(mutate_body_type_with_extractors)) .wrap(from_fn(timeout_10secs)) // switch bool to true to observe early response .wrap(MyMw(false).into_middleware()) .wrap(Logger::default()) .default_service(web::to(HttpResponse::Ok)) }) .workers(1) .bind(bind)? .run() .await } actix-web-4.9.0/examples/on-connect.rs000064400000000000000000000034311046102023000157510ustar 00000000000000//! This example shows how to use `actix_web::HttpServer::on_connect` to access a lower-level socket //! properties and pass them to a handler through request-local data. //! //! For an example of extracting a client TLS certificate, see: //! use std::{any::Any, io, net::SocketAddr}; use actix_web::{ dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, Responder, }; #[allow(dead_code)] #[derive(Debug, Clone)] struct ConnectionInfo { bind: SocketAddr, peer: SocketAddr, ttl: Option, } async fn route_whoami(req: HttpRequest) -> impl Responder { match req.conn_data::() { Some(info) => HttpResponse::Ok().body(format!( "Here is some info about your connection:\n\n{info:#?}", )), None => HttpResponse::InternalServerError().body("Missing expected request extension data"), } } fn get_conn_info(connection: &dyn Any, data: &mut Extensions) { if let Some(sock) = connection.downcast_ref::() { data.insert(ConnectionInfo { bind: sock.local_addr().unwrap(), peer: sock.peer_addr().unwrap(), ttl: sock.ttl().ok(), }); } else { unreachable!("connection should only be plaintext since no TLS is set up"); } } #[actix_web::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let bind = ("127.0.0.1", 8080); log::info!("staring server at http://{}:{}", &bind.0, &bind.1); HttpServer::new(|| App::new().default_service(web::to(route_whoami))) .on_connect(get_conn_info) .bind_auto_h2c(bind)? .workers(2) .run() .await } actix-web-4.9.0/examples/uds.rs000064400000000000000000000027111046102023000145010ustar 00000000000000use actix_web::{get, web, HttpRequest}; #[cfg(unix)] use actix_web::{middleware, App, Error, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] async fn index(req: HttpRequest, name: web::Path) -> String { println!("REQ: {:?}", req); format!("Hello: {}!\r\n", name) } #[cfg(unix)] async fn index_async(req: HttpRequest) -> Result<&'static str, Error> { println!("REQ: {:?}", req); Ok("Hello world!\r\n") } #[get("/")] async fn no_params() -> &'static str { "Hello world!\r\n" } #[cfg(unix)] #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); HttpServer::new(|| { App::new() .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .service(index) .service(no_params) .service( web::resource("/resource2/index.html") .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3"))) .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) .bind_uds("/Users/me/uds-test")? .workers(1) .run() .await } #[cfg(not(unix))] fn main() {} actix-web-4.9.0/examples/worker-cpu-pin.rs000064400000000000000000000020201046102023000165610ustar 00000000000000use std::{ io, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, thread, }; use actix_web::{middleware, web, App, HttpServer}; async fn hello() -> &'static str { "Hello world!" } #[actix_web::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let core_ids = core_affinity::get_core_ids().unwrap(); let n_core_ids = core_ids.len(); let next_core_id = Arc::new(AtomicUsize::new(0)); HttpServer::new(move || { let pin = Arc::clone(&next_core_id).fetch_add(1, Ordering::AcqRel); log::info!( "setting CPU affinity for worker {}: pinning to core {}", thread::current().name().unwrap(), pin, ); core_affinity::set_for_current(core_ids[pin]); App::new() .wrap(middleware::Logger::default()) .service(web::resource("/").get(hello)) }) .bind(("127.0.0.1", 8080))? .workers(n_core_ids) .run() .await } actix-web-4.9.0/src/app.rs000064400000000000000000000566051046102023000134520ustar 00000000000000use std::{cell::RefCell, fmt, future::Future, rc::Rc}; use actix_http::{body::MessageBody, Extensions, Request}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_util::FutureExt as _; use crate::{ app_service::{AppEntry, AppInit, AppRoutingFactory}, config::ServiceConfig, data::{Data, DataFactory, FnDataFactory}, dev::ResourceDef, error::Error, resource::Resource, route::Route, service::{ AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }, }; /// The top-level builder for an Actix Web application. pub struct App { endpoint: T, services: Vec>, default: Option>, factory_ref: Rc>>, data_factories: Vec, external: Vec, extensions: Extensions, } impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { let factory_ref = Rc::new(RefCell::new(None)); App { endpoint: AppEntry::new(Rc::clone(&factory_ref)), data_factories: Vec::new(), services: Vec::new(), default: None, factory_ref, external: Vec::new(), extensions: Extensions::new(), } } } impl App where T: ServiceFactory, { /// Set application (root level) data. /// /// Application data stored with `App::app_data()` method is available through the /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. /// /// # [`Data`] /// Any [`Data`] type added here can utilize its extractor implementation in handlers. /// Types not wrapped in `Data` cannot use this extractor. See [its docs](Data) for more /// about its usage and patterns. /// /// ``` /// use std::cell::Cell; /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { /// count: std::cell::Cell, /// } /// /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { /// // note this cannot use the Data extractor because it was not added with it /// let incr = *req.app_data::().unwrap(); /// assert_eq!(incr, 3); /// /// // update counter using other value from app data /// counter.count.set(counter.count.get() + incr); /// /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// /// let app = App::new().service( /// web::resource("/") /// .app_data(3usize) /// .app_data(web::Data::new(MyData { count: Default::default() })) /// .route(web::get().to(handler)) /// ); /// ``` /// /// # Shared Mutable State /// [`HttpServer::new`](crate::HttpServer::new) accepts an application factory rather than an /// application instance; the factory closure is called on each worker thread independently. /// Therefore, if you want to share a data object between different workers, a shareable object /// needs to be created first, outside the `HttpServer::new` closure and cloned into it. /// [`Data`] is an example of such a sharable object. /// /// ```ignore /// let counter = web::Data::new(AppStateWithCounter { /// counter: Mutex::new(0), /// }); /// /// HttpServer::new(move || { /// // move counter object into the closure and clone for each worker /// /// App::new() /// .app_data(counter.clone()) /// .route("/", web::get().to(handler)) /// }) /// ``` #[doc(alias = "manage")] pub fn app_data(mut self, data: U) -> Self { self.extensions.insert(data); self } /// Add application (root) data after wrapping in `Data`. /// /// Deprecated in favor of [`app_data`](Self::app_data). #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } /// Add application data factory that resolves asynchronously. /// /// Data items are constructed during application initialization, before the server starts /// accepting requests. /// /// The returned data value `D` is wrapped as [`Data`]. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, Out: Future> + 'static, D: 'static, E: std::fmt::Debug, { self.data_factories.push(Box::new(move || { { let fut = data(); async move { match fut.await { Err(err) => { log::error!("Can not construct data instance: {err:?}"); Err(()) } Ok(data) => { let data: Box = Box::new(Data::new(data)); Ok(data) } } } } .boxed_local() })); self } /// Run external configuration as part of the application building /// process /// /// This function is useful for moving parts of configuration to a /// different module or even library. For example, /// some of the resource's configuration could be moved to different module. /// /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// // this function could be located in different module /// fn config(cfg: &mut web::ServiceConfig) { /// cfg.service(web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// /// App::new() /// .configure(config) // <- register resources /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, f: F) -> Self where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); f(&mut cfg); self.services.extend(cfg.services); self.external.extend(cfg.external); self.extensions.extend(cfg.app_data); if let Some(default) = cfg.default { self.default = Some(default); } self } /// Configure route for a specific path. /// /// This is a simplified version of the `App::service()` method. /// This method can be used multiple times with same path, in that case /// multiple resources with one route would be registered for same resource path. /// /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// /// let app = App::new() /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( Resource::new(path) .add_guards(route.take_guards()) .route(route), ) } /// Register HTTP service. /// /// Http service is any type that implements `HttpServiceFactory` trait. /// /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. pub fn service(mut self, factory: F) -> Self where F: HttpServiceFactory + 'static, { self.services .push(Box::new(ServiceFactoryWrapper::new(factory))); self } /// Default service that is invoked when no matching resource could be found. /// /// You can use a [`Route`] as default service. /// /// If a default service is not registered, an empty `404 Not Found` response will be sent to /// the client instead. /// /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index() -> &'static str { /// "Welcome!" /// } /// /// let app = App::new() /// .service(web::resource("/index.html").route(web::get().to(index))) /// .default_service(web::to(|| HttpResponse::NotFound())); /// ``` pub fn default_service(mut self, svc: F) -> Self where F: IntoServiceFactory, U: ServiceFactory + 'static, U::InitError: fmt::Debug, { let svc = svc .into_factory() .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); self.default = Some(Rc::new(boxed::factory(svc))); self } /// Register an external resource. /// /// External resources are useful for URL generation purposes only /// and are never considered for matching at request time. Calls to /// `HttpRequest::url_for()` will work as expected. /// /// ``` /// use actix_web::{web, App, HttpRequest, HttpResponse, Result}; /// /// async fn index(req: HttpRequest) -> Result { /// let url = req.url_for("youtube", &["asdlkjqme"])?; /// assert_eq!(url.as_str(), "https://youtube.com/watch/asdlkjqme"); /// Ok(HttpResponse::Ok().into()) /// } /// /// let app = App::new() /// .service(web::resource("/index.html").route( /// web::get().to(index))) /// .external_resource("youtube", "https://youtube.com/watch/{video_id}"); /// ``` pub fn external_resource(mut self, name: N, url: U) -> Self where N: AsRef, U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); rdef.set_name(name.as_ref()); self.external.push(rdef); self } /// Registers an app-wide middleware. /// /// Registers middleware, in the form of a middleware component (type), that runs during /// inbound and/or outbound processing in the request life-cycle (request -> response), /// modifying request/response as necessary, across all requests managed by the `App`. /// /// Use middleware when you need to read or modify *every* request or response in some way. /// /// Middleware can be applied similarly to individual `Scope`s and `Resource`s. /// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`]. /// /// For more info on middleware take a look at the [`middleware` module][crate::middleware]. /// /// # Examples /// ``` /// use actix_web::{middleware, web, App}; /// /// async fn index() -> &'static str { /// "Welcome!" /// } /// /// let app = App::new() /// .wrap(middleware::Logger::default()) /// .route("/index.html", web::get().to(index)); /// ``` #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap( self, mw: M, ) -> App< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where M: Transform< T::Service, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody, { App { endpoint: apply(mw, self.endpoint), data_factories: self.data_factories, services: self.services, default: self.default, factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, } } /// Registers an app-wide function middleware. /// /// `mw` is a closure that runs during inbound and/or outbound processing in the request /// life-cycle (request -> response), modifying request/response as necessary, across all /// requests handled by the `App`. /// /// Use middleware when you need to read or modify *every* request or response in some way. /// /// Middleware can also be applied to individual `Scope`s and `Resource`s. /// /// See [`App::wrap`] for details on how middlewares compose with each other. /// /// # Examples /// ``` /// use actix_web::{dev::Service as _, middleware, web, App}; /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; /// /// async fn index() -> &'static str { /// "Welcome!" /// } /// /// let app = App::new() /// .wrap_fn(|req, srv| { /// let fut = srv.call(req); /// async { /// let mut res = fut.await?; /// res.headers_mut() /// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); /// Ok(res) /// } /// }) /// .route("/index.html", web::get().to(index)); /// ``` #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap_fn( self, mw: F, ) -> App< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, R: Future, Error>>, B: MessageBody, { App { endpoint: apply_fn_factory(self.endpoint, mw), data_factories: self.data_factories, services: self.services, default: self.default, factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, } } } impl IntoServiceFactory, Request> for App where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody, { fn into_factory(self) -> AppInit { AppInit { async_data_factories: self.data_factories.into_boxed_slice().into(), endpoint: self.endpoint, services: Rc::new(RefCell::new(self.services)), external: RefCell::new(self.external), default: self.default, factory_ref: self.factory_ref, extensions: RefCell::new(Some(self.extensions)), } } } #[cfg(test)] mod tests { use actix_service::Service as _; use actix_utils::future::{err, ok}; use bytes::Bytes; use super::*; use crate::{ http::{ header::{self, HeaderValue}, Method, StatusCode, }, middleware::DefaultHeaders, test::{call_service, init_service, read_body, try_init_service, TestRequest}, web, HttpRequest, HttpResponse, }; #[actix_rt::test] async fn test_default_resource() { let srv = init_service(App::new().service(web::resource("/test").to(HttpResponse::Ok))).await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/blah").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let srv = init_service( App::new() .service(web::resource("/test").to(HttpResponse::Ok)) .service( web::resource("/test2") .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::Created())) }) .route(web::get().to(HttpResponse::Ok)), ) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) }), ) .await; let req = TestRequest::with_uri("/blah").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/test2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test2") .method(Method::POST) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory() { let srv = init_service( App::new() .data_factory(|| ok::<_, ()>(10usize)) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let srv = init_service( App::new() .data_factory(|| ok::<_, ()>(10u32)) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory_errors() { let srv = try_init_service( App::new() .data_factory(|| err::(())) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; assert!(srv.is_err()); } #[actix_rt::test] async fn test_extension() { let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( |req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 10); HttpResponse::Ok() }, ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_wrap() { let srv = init_service( App::new() .wrap( DefaultHeaders::new() .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .route("/test", web::get().to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_router_wrap() { let srv = init_service( App::new() .route("/test", web::get().to(HttpResponse::Ok)) .wrap( DefaultHeaders::new() .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_wrap_fn() { let srv = init_service( App::new() .wrap_fn(|req, srv| { let fut = srv.call(req); async move { let mut res = fut.await?; res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_router_wrap_fn() { let srv = init_service( App::new() .route("/test", web::get().to(HttpResponse::Ok)) .wrap_fn(|req, srv| { let fut = srv.call(req); async { let mut res = fut.await?; res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_external_resource() { let srv = init_service( App::new() .external_resource("youtube", "https://youtube.com/watch/{video_id}") .route( "/test", web::get().to(|req: HttpRequest| { HttpResponse::Ok() .body(req.url_for("youtube", ["12345"]).unwrap().to_string()) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); } #[test] fn can_be_returned_from_fn() { /// compile-only test for returning app type from function pub fn my_app() -> App< impl ServiceFactory< ServiceRequest, Response = ServiceResponse, Config = (), InitError = (), Error = Error, >, > { App::new() // logger can be removed without affecting the return type .wrap(crate::middleware::Logger::default()) .route("/", web::to(|| async { "hello" })) } #[allow(clippy::let_underscore_future)] let _ = init_service(my_app()); } } actix-web-4.9.0/src/app_service.rs000064400000000000000000000265101046102023000151620ustar 00000000000000use std::{cell::RefCell, mem, rc::Rc}; use actix_http::Request; use actix_router::{Path, ResourceDef, Router, Url}; use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ body::BoxBody, config::{AppConfig, AppService}, data::FnDataFactory, dev::Extensions, guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, service::{ AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, ServiceRequest, ServiceResponse, }, Error, HttpResponse, }; /// Service factory to convert [`Request`] to a [`ServiceRequest`]. /// /// It also executes data factories. pub struct AppInit where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, { pub(crate) endpoint: T, pub(crate) extensions: RefCell>, pub(crate) async_data_factories: Rc<[FnDataFactory]>, pub(crate) services: Rc>>>, pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, } impl ServiceFactory for AppInit where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, T::Future: 'static, { type Response = ServiceResponse; type Error = T::Error; type Config = AppConfig; type Service = AppInitService; type InitError = T::InitError; type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, config: AppConfig) -> Self::Future { // set AppService's default service to 404 NotFound // if no user defined default service exists. let default = self.default.clone().unwrap_or_else(|| { Rc::new(boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::NotFound())) }))) }); // create App config to pass to child services let mut config = AppService::new(config, Rc::clone(&default)); // register services mem::take(&mut *self.services.borrow_mut()) .into_iter() .for_each(|mut srv| srv.register(&mut config)); let mut rmap = ResourceMap::new(ResourceDef::prefix("")); let (config, services) = config.into_services(); // complete pipeline creation. *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default, services: services .into_iter() .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); (rdef, srv, RefCell::new(guards)) }) .collect::>() .into_boxed_slice() .into(), }); // external resources for mut rdef in mem::take(&mut *self.external.borrow_mut()) { rmap.add(&mut rdef, None); } // complete ResourceMap tree creation let rmap = Rc::new(rmap); ResourceMap::finish(&rmap); // construct all async data factory futures let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); // construct app service and middleware service factory future. let endpoint_fut = self.endpoint.new_service(()); // take extensions or create new one as app data container. let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default(); Box::pin(async move { // async data factories let async_data_factories = factory_futs .await .into_iter() .collect::, _>>() .map_err(|_| ())?; // app service and middleware let service = endpoint_fut.await?; // populate app data container from (async) data factories. for factory in &async_data_factories { factory.create(&mut app_data); } Ok(AppInitService { service, app_data: Rc::new(app_data), app_state: AppInitServiceState::new(rmap, config), }) }) } } /// The [`Service`] that is passed to `actix-http`'s server builder. /// /// Wraps a service receiving a [`ServiceRequest`] into one receiving a [`Request`]. pub struct AppInitService where T: Service, Error = Error>, { service: T, app_data: Rc, app_state: Rc, } /// A collection of state for [`AppInitService`] that is shared across [`HttpRequest`]s. pub(crate) struct AppInitServiceState { rmap: Rc, config: AppConfig, pool: HttpRequestPool, } impl AppInitServiceState { /// Constructs state collection from resource map and app config. pub(crate) fn new(rmap: Rc, config: AppConfig) -> Rc { Rc::new(AppInitServiceState { rmap, config, pool: HttpRequestPool::default(), }) } /// Returns a reference to the application's resource map. #[inline] pub(crate) fn rmap(&self) -> &ResourceMap { &self.rmap } /// Returns a reference to the application's configuration. #[inline] pub(crate) fn config(&self) -> &AppConfig { &self.config } /// Returns a reference to the application's request pool. #[inline] pub(crate) fn pool(&self) -> &HttpRequestPool { &self.pool } } impl Service for AppInitService where T: Service, Error = Error>, { type Response = ServiceResponse; type Error = T::Error; type Future = T::Future; actix_service::forward_ready!(service); fn call(&self, mut req: Request) -> Self::Future { let extensions = Rc::new(RefCell::new(req.take_req_data())); let conn_data = req.take_conn_data(); let (head, payload) = req.into_parts(); let req = match self.app_state.pool().pop() { Some(mut req) => { let inner = Rc::get_mut(&mut req.inner).unwrap(); inner.path.get_mut().update(&head.uri); inner.path.reset(); inner.head = head; inner.conn_data = conn_data; inner.extensions = extensions; req } None => HttpRequest::new( Path::new(Url::new(head.uri.clone())), head, Rc::clone(&self.app_state), Rc::clone(&self.app_data), conn_data, extensions, ), }; self.service.call(ServiceRequest::new(req, payload)) } } impl Drop for AppInitService where T: Service, Error = Error>, { fn drop(&mut self) { self.app_state.pool().clear(); } } pub struct AppRoutingFactory { #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, BoxedHttpServiceFactory, RefCell>>>, )], >, default: Rc, } impl ServiceFactory for AppRoutingFactory { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = AppRouting; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { // construct all services factory future with its resource def and guards. let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { let path = path.clone(); let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { factory_fut .await .map(move |service| (path, guards, service)) } })); // construct default service factory future let default_fut = self.default.new_service(()); Box::pin(async move { let default = default_fut.await?; // build router from the factory future result. let router = factory_fut .await .into_iter() .collect::, _>>()? .drain(..) .fold(Router::build(), |mut router, (path, guards, service)| { router.push(path, service, guards); router }) .finish(); Ok(AppRouting { router, default }) }) } } /// The Actix Web router default entry point. pub struct AppRouting { router: Router>>, default: BoxedHttpService, } impl Service for AppRouting { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { let guard_ctx = req.guard_ctx(); guards.iter().all(|guard| guard.check(&guard_ctx)) }); if let Some((srv, _info)) = res { srv.call(req) } else { self.default.call(req) } } } /// Wrapper service for routing pub struct AppEntry { factory: Rc>>, } impl AppEntry { pub fn new(factory: Rc>>) -> Self { AppEntry { factory } } } impl ServiceFactory for AppEntry { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = AppRouting; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(()) } } #[cfg(test)] mod tests { use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use actix_service::Service; use crate::{ test::{init_service, TestRequest}, web, App, HttpResponse, }; struct DropData(Arc); impl Drop for DropData { fn drop(&mut self) { self.0.store(true, Ordering::Relaxed); } } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_drop_data() { let data = Arc::new(AtomicBool::new(false)); { let app = init_service( App::new() .data(DropData(data.clone())) .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); let _ = app.call(req).await.unwrap(); } assert!(data.load(Ordering::Relaxed)); } } actix-web-4.9.0/src/config.rs000064400000000000000000000321121046102023000141220ustar 00000000000000use std::{net::SocketAddr, rc::Rc}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use crate::{ data::Data, dev::{Extensions, ResourceDef}, error::Error, guard::Guard, resource::Resource, rmap::ResourceMap, route::Route, service::{ AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }, }; type Guards = Vec>; /// Application configuration pub struct AppService { config: AppConfig, root: bool, default: Rc, #[allow(clippy::type_complexity)] services: Vec<( ResourceDef, BoxedHttpServiceFactory, Option, Option>, )>, } impl AppService { /// Crate server settings instance. pub(crate) fn new(config: AppConfig, default: Rc) -> Self { AppService { config, default, root: true, services: Vec::new(), } } /// Check if root is being configured pub fn is_root(&self) -> bool { self.root } #[allow(clippy::type_complexity)] pub(crate) fn into_services( self, ) -> ( AppConfig, Vec<( ResourceDef, BoxedHttpServiceFactory, Option, Option>, )>, ) { (self.config, self.services) } /// Clones inner config and default service, returning new `AppService` with empty service list /// marked as non-root. pub(crate) fn clone_config(&self) -> Self { AppService { config: self.config.clone(), default: Rc::clone(&self.default), services: Vec::new(), root: false, } } /// Returns reference to configuration. pub fn config(&self) -> &AppConfig { &self.config } /// Returns default handler factory. pub fn default_service(&self) -> Rc { Rc::clone(&self.default) } /// Register HTTP service. pub fn register_service( &mut self, rdef: ResourceDef, guards: Option>>, factory: F, nested: Option>, ) where F: IntoServiceFactory, S: ServiceFactory< ServiceRequest, Response = ServiceResponse, Error = Error, Config = (), InitError = (), > + 'static, { self.services .push((rdef, boxed::factory(factory.into_factory()), guards, nested)); } } /// Application connection config. #[derive(Debug, Clone)] pub struct AppConfig { secure: bool, host: String, addr: SocketAddr, } impl AppConfig { pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig { secure, host, addr } } /// Needed in actix-test crate. Semver exempt. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig::new(secure, host, addr) } /// Server host name. /// /// Host name is used by application router as a hostname for URL generation. /// Check [ConnectionInfo](super::dev::ConnectionInfo::host()) /// documentation for more information. /// /// By default host name is set to a "localhost" value. pub fn host(&self) -> &str { &self.host } /// Returns true if connection is secure (i.e., running over `https:`). pub fn secure(&self) -> bool { self.secure } /// Returns the socket address of the local half of this TCP connection. pub fn local_addr(&self) -> SocketAddr { self.addr } #[cfg(test)] pub(crate) fn set_host(&mut self, host: &str) { host.clone_into(&mut self.host); } } impl Default for AppConfig { /// Returns the default AppConfig. /// Note: The included socket address is "127.0.0.1". /// /// 127.0.0.1: non-routable meta address that denotes an unknown, invalid or non-applicable target. /// If you need a service only accessed by itself, use a loopback address. /// A loopback address for IPv4 is any loopback address that begins with "127". /// Loopback addresses should be only used to test your application locally. /// The default configuration provides a loopback address. /// /// 0.0.0.0: if configured to use this special address, the application will listen to any IP address configured on the machine. fn default() -> Self { AppConfig::new( false, "localhost:8080".to_owned(), "127.0.0.1:8080".parse().unwrap(), ) } } /// Enables parts of app configuration to be declared separately from the app itself. Helpful for /// modularizing large applications. /// /// Merge a `ServiceConfig` into an app using [`App::configure`](crate::App::configure). Scope and /// resources services have similar methods. /// /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// // this function could be located in different module /// fn config(cfg: &mut web::ServiceConfig) { /// cfg.service(web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// /// // merge `/test` routes from config function to App /// App::new().configure(config); /// ``` pub struct ServiceConfig { pub(crate) services: Vec>, pub(crate) external: Vec, pub(crate) app_data: Extensions, pub(crate) default: Option>, } impl ServiceConfig { pub(crate) fn new() -> Self { Self { services: Vec::new(), external: Vec::new(), app_data: Extensions::new(), default: None, } } /// Add shared app data item. /// /// Counterpart to [`App::data()`](crate::App::data). #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(&mut self, data: U) -> &mut Self { self.app_data(Data::new(data)); self } /// Add arbitrary app data item. /// /// Counterpart to [`App::app_data()`](crate::App::app_data). pub fn app_data(&mut self, ext: U) -> &mut Self { self.app_data.insert(ext); self } /// Default service to be used if no matching resource could be found. /// /// Counterpart to [`App::default_service()`](crate::App::default_service). pub fn default_service(&mut self, f: F) -> &mut Self where F: IntoServiceFactory, U: ServiceFactory + 'static, U::InitError: std::fmt::Debug, { let svc = f .into_factory() .map_init_err(|err| log::error!("Can not construct default service: {:?}", err)); self.default = Some(Rc::new(boxed::factory(svc))); self } /// Run external configuration as part of the application building process /// /// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting. pub fn configure(&mut self, f: F) -> &mut Self where F: FnOnce(&mut ServiceConfig), { f(self); self } /// Configure route for a specific path. /// /// Counterpart to [`App::route()`](crate::App::route). pub fn route(&mut self, path: &str, mut route: Route) -> &mut Self { self.service( Resource::new(path) .add_guards(route.take_guards()) .route(route), ) } /// Register HTTP service factory. /// /// Counterpart to [`App::service()`](crate::App::service). pub fn service(&mut self, factory: F) -> &mut Self where F: HttpServiceFactory + 'static, { self.services .push(Box::new(ServiceFactoryWrapper::new(factory))); self } /// Register an external resource. /// /// External resources are useful for URL generation purposes only and are never considered for /// matching at request time. Calls to [`HttpRequest::url_for()`](crate::HttpRequest::url_for) /// will work as expected. /// /// Counterpart to [`App::external_resource()`](crate::App::external_resource). pub fn external_resource(&mut self, name: N, url: U) -> &mut Self where N: AsRef, U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); rdef.set_name(name.as_ref()); self.external.push(rdef); self } } #[cfg(test)] mod tests { use actix_service::Service; use bytes::Bytes; use super::*; use crate::{ http::{Method, StatusCode}, test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpRequest, HttpResponse, }; // allow deprecated `ServiceConfig::data` #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let cfg = |cfg: &mut ServiceConfig| { cfg.data(10usize); cfg.app_data(15u8); }; let srv = init_service(App::new().configure(cfg).service(web::resource("/").to( |_: web::Data, req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 15u8); HttpResponse::Ok() }, ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_external_resource() { let srv = init_service( App::new() .configure(|cfg| { cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}"); }) .route( "/test", web::get().to(|req: HttpRequest| { HttpResponse::Ok() .body(req.url_for("youtube", ["12345"]).unwrap().to_string()) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); } #[actix_rt::test] async fn registers_default_service() { let srv = init_service( App::new() .configure(|cfg| { cfg.default_service( web::get().to(|| HttpResponse::NotFound().body("four oh four")), ); }) .service(web::scope("/scoped").configure(|cfg| { cfg.default_service( web::get().to(|| HttpResponse::NotFound().body("scoped four oh four")), ); })), ) .await; // app registers default service let req = TestRequest::with_uri("/path/i/did/not-configure").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"four oh four")); // scope registers default service let req = TestRequest::with_uri("/scoped/path/i/did/not-configure").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"scoped four oh four")); } #[actix_rt::test] async fn test_service() { let srv = init_service(App::new().configure(|cfg| { cfg.service(web::resource("/test").route(web::get().to(HttpResponse::Created))) .route("/index.html", web::get().to(HttpResponse::Ok)); })) .await; let req = TestRequest::with_uri("/test") .method(Method::GET) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/index.html") .method(Method::GET) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn nested_service_configure() { fn cfg_root(cfg: &mut ServiceConfig) { cfg.configure(cfg_sub); } fn cfg_sub(cfg: &mut ServiceConfig) { cfg.route("/", web::get().to(|| async { "hello world" })); } let srv = init_service(App::new().configure(cfg_root)).await; let req = TestRequest::with_uri("/").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_body_eq!(res, b"hello world"); } } actix-web-4.9.0/src/data.rs000064400000000000000000000276661046102023000136100ustar 00000000000000use std::{any::type_name, ops::Deref, sync::Arc}; use actix_http::Extensions; use actix_utils::future::{err, ok, Ready}; use futures_core::future::LocalBoxFuture; use serde::{de, Serialize}; use crate::{dev::Payload, error, Error, FromRequest, HttpRequest}; /// Data factory. pub(crate) trait DataFactory { /// Return true if modifications were made to extensions map. fn create(&self, extensions: &mut Extensions) -> bool; } pub(crate) type FnDataFactory = Box LocalBoxFuture<'static, Result, ()>>>; /// Application data wrapper and extractor. /// /// # Setting Data /// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped /// in this `Data` type for those calls, it can be used as an extractor. /// /// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared, /// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`. /// /// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data), /// and [`Resource::app_data`](crate::Resource::app_data). /// /// # Extracting `Data` /// Since the Actix Web router layers application data, the returned object will reference the /// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope` /// also stores a `u32`, and the delegated request handler falls within that `Scope`, then /// extracting a `web::Data` for that handler will return the `Scope`'s instance. However, /// using the same router set up and a request that does not get captured by the `Scope`, /// `web::>` would return the `App`'s instance. /// /// If route data is not set for a handler, using `Data` extractor would cause a `500 Internal /// Server Error` response. /// /// See also [`HttpRequest::app_data`] /// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data). /// /// # Unsized Data /// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first /// constructing an `Arc` and using the `From` implementation to convert it. /// /// ``` /// # use std::{fmt::Display, sync::Arc}; /// # use actix_web::web::Data; /// let displayable_arc: Arc = Arc::new(42usize); /// let displayable_data: Data = Data::from(displayable_arc); /// ``` /// /// # Examples /// ``` /// use std::sync::Mutex; /// use actix_web::{App, HttpRequest, HttpResponse, Responder, web::{self, Data}}; /// /// struct MyData { /// counter: usize, /// } /// /// /// Use the `Data` extractor to access data in a handler. /// async fn index(data: Data>) -> impl Responder { /// let mut my_data = data.lock().unwrap(); /// my_data.counter += 1; /// HttpResponse::Ok() /// } /// /// /// Alternatively, use the `HttpRequest::app_data` method to access data in a handler. /// async fn index_alt(req: HttpRequest) -> impl Responder { /// let data = req.app_data::>>().unwrap(); /// let mut my_data = data.lock().unwrap(); /// my_data.counter += 1; /// HttpResponse::Ok() /// } /// /// let data = Data::new(Mutex::new(MyData { counter: 0 })); /// /// let app = App::new() /// // Store `MyData` in application storage. /// .app_data(Data::clone(&data)) /// .route("/index.html", web::get().to(index)) /// .route("/index-alt.html", web::get().to(index_alt)); /// ``` #[doc(alias = "state")] #[derive(Debug)] pub struct Data(Arc); impl Data { /// Create new `Data` instance. pub fn new(state: T) -> Data { Data(Arc::new(state)) } } impl Data { /// Returns reference to inner `T`. pub fn get_ref(&self) -> &T { self.0.as_ref() } /// Unwraps to the internal `Arc` pub fn into_inner(self) -> Arc { self.0 } } impl Deref for Data { type Target = Arc; fn deref(&self) -> &Arc { &self.0 } } impl Clone for Data { fn clone(&self) -> Data { Data(Arc::clone(&self.0)) } } impl From> for Data { fn from(arc: Arc) -> Self { Data(arc) } } impl Default for Data { fn default() -> Self { Data::new(T::default()) } } impl Serialize for Data where T: Serialize, { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.serialize(serializer) } } impl<'de, T> de::Deserialize<'de> for Data where T: de::Deserialize<'de>, { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { Ok(Data::new(T::deserialize(deserializer)?)) } } impl FromRequest for Data { type Error = Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { if let Some(st) = req.app_data::>() { ok(st.clone()) } else { log::debug!( "Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \ correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \ Ensure that types align in both the set and retrieve calls.", type_name::(), req.match_name().unwrap_or_else(|| req.path()) ); err(error::ErrorInternalServerError( "Requested application data is not configured correctly. \ View/enable debug logs for more details.", )) } } } impl DataFactory for Data { fn create(&self, extensions: &mut Extensions) -> bool { extensions.insert(Data(Arc::clone(&self.0))); true } } #[cfg(test)] mod tests { use super::*; use crate::{ dev::Service, http::StatusCode, test::{init_service, TestRequest}, web, App, HttpResponse, }; // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { let srv = init_service( App::new() .data("TEST".to_string()) .service(web::resource("/").to(|data: web::Data| { assert_eq!(data.to_lowercase(), "test"); HttpResponse::Ok() })), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let srv = init_service( App::new() .data(10u32) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); let srv = init_service( App::new() .data(10u32) .data(13u32) .app_data(12u64) .app_data(15u64) .default_service(web::to(|n: web::Data, req: HttpRequest| { // in each case, the latter insertion should be preserved assert_eq!(*req.app_data::().unwrap(), 15); assert_eq!(*n.into_inner(), 13); HttpResponse::Ok() })), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_app_data_extractor() { let srv = init_service( App::new() .app_data(Data::new(10usize)) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let srv = init_service( App::new() .app_data(Data::new(10u32)) .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_route_data_extractor() { let srv = init_service( App::new().service( web::resource("/") .data(10usize) .route(web::get().to(|_data: web::Data| HttpResponse::Ok())), ), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // different type let srv = init_service( App::new().service( web::resource("/") .data(10u32) .route(web::get().to(|_: web::Data| HttpResponse::Ok())), ), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = init_service( App::new() .data(1usize) .service(web::resource("/").data(10usize).route(web::get().to( |data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() }, ))), ) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_data_from_arc() { let data_new = Data::new(String::from("test-123")); let data_from_arc = Data::from(Arc::new(String::from("test-123"))); assert_eq!(data_new.0, data_from_arc.0); } #[actix_rt::test] async fn test_data_from_dyn_arc() { trait TestTrait { fn get_num(&self) -> i32; } struct A {} impl TestTrait for A { fn get_num(&self) -> i32 { 42 } } // This works when Sized is required let dyn_arc_box: Arc> = Arc::new(Box::new(A {})); let data_arc_box = Data::from(dyn_arc_box); // This works when Data Sized Bound is removed let dyn_arc: Arc = Arc::new(A {}); let data_arc = Data::from(dyn_arc); assert_eq!(data_arc_box.get_num(), data_arc.get_num()) } #[actix_rt::test] async fn test_dyn_data_into_arc() { trait TestTrait { fn get_num(&self) -> i32; } struct A {} impl TestTrait for A { fn get_num(&self) -> i32 { 42 } } let dyn_arc: Arc = Arc::new(A {}); let data_arc = Data::from(dyn_arc); let arc_from_data = data_arc.clone().into_inner(); assert_eq!(data_arc.get_num(), arc_from_data.get_num()) } #[actix_rt::test] async fn test_get_ref_from_dyn_data() { trait TestTrait { fn get_num(&self) -> i32; } struct A {} impl TestTrait for A { fn get_num(&self) -> i32 { 42 } } let dyn_arc: Arc = Arc::new(A {}); let data_arc = Data::from(dyn_arc); let ref_data = data_arc.get_ref(); assert_eq!(data_arc.get_num(), ref_data.get_num()) } } actix-web-4.9.0/src/dev.rs000064400000000000000000000030231046102023000134320ustar 00000000000000//! Lower-level types and re-exports. //! //! Most users will not have to interact with the types in this module, but it is useful for those //! writing extractors, middleware, libraries, or interacting with the service API directly. //! //! # Request Extractors //! - [`ConnectionInfo`]: Connection information //! - [`PeerAddr`]: Connection information #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; use actix_router::Patterns; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::{Server, ServerHandle}; pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; #[doc(hidden)] pub use crate::handler::Handler; pub use crate::{ config::{AppConfig, AppService}, info::{ConnectionInfo, PeerAddr}, rmap::ResourceMap, service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}, types::{JsonBody, Readlines, UrlEncoded}, }; pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { match &mut patterns { Patterns::Single(pat) => { if !pat.is_empty() && !pat.starts_with('/') { pat.insert(0, '/'); }; } Patterns::List(pats) => { for pat in pats { if !pat.is_empty() && !pat.starts_with('/') { pat.insert(0, '/'); }; } } } patterns } actix-web-4.9.0/src/error/error.rs000064400000000000000000000037771046102023000151560ustar 00000000000000use std::{error::Error as StdError, fmt}; use actix_http::{body::BoxBody, Response}; use crate::{HttpResponse, ResponseError}; /// General purpose Actix Web error. /// /// An Actix Web error is used to carry errors from `std::error` through actix in a convenient way. /// It can be created through converting errors with `into()`. /// /// Whenever it is created from an external object a response error is created for it that can be /// used to create an HTTP response from it this means that if you have access to an actix `Error` /// you can always get a `ResponseError` reference from it. pub struct Error { cause: Box, } impl Error { /// Returns the reference to the underlying `ResponseError`. pub fn as_response_error(&self) -> &dyn ResponseError { self.cause.as_ref() } /// Similar to `as_response_error` but downcasts. pub fn as_error(&self) -> Option<&T> { ::downcast_ref(self.cause.as_ref()) } /// Shortcut for creating an `HttpResponse`. pub fn error_response(&self) -> HttpResponse { self.cause.error_response() } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.cause, f) } } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", &self.cause) } } impl StdError for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } /// `Error` for any error that implements `ResponseError` impl From for Error { fn from(err: T) -> Error { Error { cause: Box::new(err), } } } impl From> for Error { fn from(value: Box) -> Self { Error { cause: value } } } impl From for Response { fn from(err: Error) -> Response { err.error_response().into() } } actix-web-4.9.0/src/error/internal.rs000064400000000000000000000266211046102023000156320ustar 00000000000000use std::{cell::RefCell, fmt, io::Write as _}; use actix_http::{ body::BoxBody, header::{self, TryIntoHeaderValue as _}, StatusCode, }; use bytes::{BufMut as _, BytesMut}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; /// Wraps errors to alter the generated response status code. /// /// In following example, the `io::Error` is wrapped into `ErrorBadRequest` which will generate a /// response with the 400 Bad Request status code instead of the usual status code generated by /// an `io::Error`. /// /// # Examples /// ``` /// # use std::io; /// # use actix_web::{error, HttpRequest}; /// async fn handler_error() -> Result { /// let err = io::Error::new(io::ErrorKind::Other, "error"); /// Err(error::ErrorBadRequest(err)) /// } /// ``` pub struct InternalError { cause: T, status: InternalErrorType, } enum InternalErrorType { Status(StatusCode), Response(RefCell>), } impl InternalError { /// Constructs an `InternalError` with given status code. pub fn new(cause: T, status: StatusCode) -> Self { InternalError { cause, status: InternalErrorType::Status(status), } } /// Constructs an `InternalError` with pre-defined response. pub fn from_response(cause: T, response: HttpResponse) -> Self { InternalError { cause, status: InternalErrorType::Response(RefCell::new(Some(response))), } } } impl fmt::Debug for InternalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.cause.fmt(f) } } impl fmt::Display for InternalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.cause.fmt(f) } } impl ResponseError for InternalError where T: fmt::Debug + fmt::Display, { fn status_code(&self) -> StatusCode { match self.status { InternalErrorType::Status(st) => st, InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.borrow().as_ref() { resp.head().status } else { StatusCode::INTERNAL_SERVER_ERROR } } } } fn error_response(&self) -> HttpResponse { match self.status { InternalErrorType::Status(status) => { let mut res = HttpResponse::new(status); let mut buf = BytesMut::new().writer(); let _ = write!(buf, "{}", self); let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); res.headers_mut().insert(header::CONTENT_TYPE, mime); res.set_body(BoxBody::new(buf.into_inner())) } InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.borrow_mut().take() { resp } else { HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) } } } } } impl Responder for InternalError where T: fmt::Debug + fmt::Display + 'static, { type Body = BoxBody; fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from_error(self) } } macro_rules! error_helper { ($name:ident, $status:ident) => { #[doc = concat!("Helper function that wraps any error and generates a `", stringify!($status), "` response.")] #[allow(non_snake_case)] pub fn $name(err: T) -> Error where T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::$status).into() } }; } error_helper!(ErrorBadRequest, BAD_REQUEST); error_helper!(ErrorUnauthorized, UNAUTHORIZED); error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED); error_helper!(ErrorForbidden, FORBIDDEN); error_helper!(ErrorNotFound, NOT_FOUND); error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED); error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE); error_helper!( ErrorProxyAuthenticationRequired, PROXY_AUTHENTICATION_REQUIRED ); error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT); error_helper!(ErrorConflict, CONFLICT); error_helper!(ErrorGone, GONE); error_helper!(ErrorLengthRequired, LENGTH_REQUIRED); error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE); error_helper!(ErrorUriTooLong, URI_TOO_LONG); error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE); error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE); error_helper!(ErrorImATeapot, IM_A_TEAPOT); error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST); error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY); error_helper!(ErrorLocked, LOCKED); error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY); error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED); error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED); error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED); error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS); error_helper!( ErrorRequestHeaderFieldsTooLarge, REQUEST_HEADER_FIELDS_TOO_LARGE ); error_helper!( ErrorUnavailableForLegalReasons, UNAVAILABLE_FOR_LEGAL_REASONS ); error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED); error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR); error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED); error_helper!(ErrorBadGateway, BAD_GATEWAY); error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE); error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT); error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED); error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES); error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE); error_helper!(ErrorLoopDetected, LOOP_DETECTED); error_helper!(ErrorNotExtended, NOT_EXTENDED); error_helper!( ErrorNetworkAuthenticationRequired, NETWORK_AUTHENTICATION_REQUIRED ); #[cfg(test)] mod tests { use actix_http::error::ParseError; use super::*; #[test] fn test_internal_error() { let err = InternalError::from_response(ParseError::Method, HttpResponse::Ok().finish()); let resp: HttpResponse = err.error_response(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_error_helpers() { let res: HttpResponse = ErrorBadRequest("err").into(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); let res: HttpResponse = ErrorUnauthorized("err").into(); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); let res: HttpResponse = ErrorPaymentRequired("err").into(); assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED); let res: HttpResponse = ErrorForbidden("err").into(); assert_eq!(res.status(), StatusCode::FORBIDDEN); let res: HttpResponse = ErrorNotFound("err").into(); assert_eq!(res.status(), StatusCode::NOT_FOUND); let res: HttpResponse = ErrorMethodNotAllowed("err").into(); assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); let res: HttpResponse = ErrorNotAcceptable("err").into(); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE); let res: HttpResponse = ErrorProxyAuthenticationRequired("err").into(); assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); let res: HttpResponse = ErrorRequestTimeout("err").into(); assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT); let res: HttpResponse = ErrorConflict("err").into(); assert_eq!(res.status(), StatusCode::CONFLICT); let res: HttpResponse = ErrorGone("err").into(); assert_eq!(res.status(), StatusCode::GONE); let res: HttpResponse = ErrorLengthRequired("err").into(); assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED); let res: HttpResponse = ErrorPreconditionFailed("err").into(); assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED); let res: HttpResponse = ErrorPayloadTooLarge("err").into(); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); let res: HttpResponse = ErrorUriTooLong("err").into(); assert_eq!(res.status(), StatusCode::URI_TOO_LONG); let res: HttpResponse = ErrorUnsupportedMediaType("err").into(); assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); let res: HttpResponse = ErrorRangeNotSatisfiable("err").into(); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); let res: HttpResponse = ErrorExpectationFailed("err").into(); assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED); let res: HttpResponse = ErrorImATeapot("err").into(); assert_eq!(res.status(), StatusCode::IM_A_TEAPOT); let res: HttpResponse = ErrorMisdirectedRequest("err").into(); assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST); let res: HttpResponse = ErrorUnprocessableEntity("err").into(); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); let res: HttpResponse = ErrorLocked("err").into(); assert_eq!(res.status(), StatusCode::LOCKED); let res: HttpResponse = ErrorFailedDependency("err").into(); assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY); let res: HttpResponse = ErrorUpgradeRequired("err").into(); assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED); let res: HttpResponse = ErrorPreconditionRequired("err").into(); assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED); let res: HttpResponse = ErrorTooManyRequests("err").into(); assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); let res: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into(); assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); let res: HttpResponse = ErrorUnavailableForLegalReasons("err").into(); assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); let res: HttpResponse = ErrorInternalServerError("err").into(); assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); let res: HttpResponse = ErrorNotImplemented("err").into(); assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED); let res: HttpResponse = ErrorBadGateway("err").into(); assert_eq!(res.status(), StatusCode::BAD_GATEWAY); let res: HttpResponse = ErrorServiceUnavailable("err").into(); assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); let res: HttpResponse = ErrorGatewayTimeout("err").into(); assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT); let res: HttpResponse = ErrorHttpVersionNotSupported("err").into(); assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); let res: HttpResponse = ErrorVariantAlsoNegotiates("err").into(); assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); let res: HttpResponse = ErrorInsufficientStorage("err").into(); assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE); let res: HttpResponse = ErrorLoopDetected("err").into(); assert_eq!(res.status(), StatusCode::LOOP_DETECTED); let res: HttpResponse = ErrorNotExtended("err").into(); assert_eq!(res.status(), StatusCode::NOT_EXTENDED); let res: HttpResponse = ErrorNetworkAuthenticationRequired("err").into(); assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); } } actix-web-4.9.0/src/error/macros.rs000064400000000000000000000101631046102023000152740ustar 00000000000000macro_rules! downcast_get_type_id { () => { /// A helper method to get the type ID of the type /// this trait is implemented on. /// This method is unsafe to *implement*, since `downcast_ref` relies /// on the returned `TypeId` to perform a cast. /// /// Unfortunately, Rust has no notion of a trait method that is /// unsafe to implement (marking it as `unsafe` makes it unsafe /// to *call*). As a workaround, we require this method /// to return a private type along with the `TypeId`. This /// private type (`PrivateHelper`) has a private constructor, /// making it impossible for safe code to construct outside of /// this module. This ensures that safe code cannot violate /// type-safety by implementing this method. /// /// We also take `PrivateHelper` as a parameter, to ensure that /// safe code cannot obtain a `PrivateHelper` instance by /// delegating to an existing implementation of `__private_get_type_id__` #[doc(hidden)] #[allow(dead_code)] fn __private_get_type_id__(&self, _: PrivateHelper) -> (std::any::TypeId, PrivateHelper) where Self: 'static, { (std::any::TypeId::of::(), PrivateHelper(())) } }; } // Generate implementation for dyn $name macro_rules! downcast_dyn { ($name:ident) => { /// A struct with a private constructor, for use with /// `__private_get_type_id__`. Its single field is private, /// ensuring that it can only be constructed from this module #[doc(hidden)] #[allow(dead_code)] pub struct PrivateHelper(()); impl dyn $name + 'static { /// Downcasts generic body to a specific type. #[allow(dead_code)] pub fn downcast_ref(&self) -> Option<&T> { if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() { // SAFETY: external crates cannot override the default // implementation of `__private_get_type_id__`, since // it requires returning a private type. We can therefore // rely on the returned `TypeId`, which ensures that this // case is correct. unsafe { Some(&*(self as *const dyn $name as *const T)) } } else { None } } /// Downcasts a generic body to a mutable specific type. #[allow(dead_code)] pub fn downcast_mut(&mut self) -> Option<&mut T> { if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() { // SAFETY: external crates cannot override the default // implementation of `__private_get_type_id__`, since // it requires returning a private type. We can therefore // rely on the returned `TypeId`, which ensures that this // case is correct. unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) } } else { None } } } }; } pub(crate) use downcast_dyn; pub(crate) use downcast_get_type_id; #[cfg(test)] mod tests { #![allow(clippy::upper_case_acronyms)] trait MB { downcast_get_type_id!(); } downcast_dyn!(MB); impl MB for String {} impl MB for () {} #[actix_rt::test] async fn test_any_casting() { let mut body = String::from("hello cast"); let resp_body: &mut dyn MB = &mut body; let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); let body = resp_body.downcast_mut::().unwrap(); body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); let not_body = resp_body.downcast_ref::<()>(); assert!(not_body.is_none()); } } actix-web-4.9.0/src/error/mod.rs000064400000000000000000000203411046102023000145660ustar 00000000000000//! Error and Result module // This is meant to be a glob import of the whole error module except for `Error`. Rustdoc can't yet // correctly resolve the conflicting `Error` type defined in this module, so these re-exports are // expanded manually. // // See pub use actix_http::error::{ContentTypeError, DispatchError, HttpError, ParseError, PayloadError}; use derive_more::{Display, Error, From}; use serde_json::error::Error as JsonError; use serde_urlencoded::{de::Error as FormDeError, ser::Error as FormError}; use url::ParseError as UrlParseError; use crate::http::StatusCode; #[allow(clippy::module_inception)] mod error; mod internal; mod macros; mod response_error; pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id}; pub use self::{error::Error, internal::*, response_error::ResponseError}; /// A convenience [`Result`](std::result::Result) for Actix Web operations. /// /// This type alias is generally used to avoid writing out `actix_http::Error` directly. pub type Result = std::result::Result; /// An error representing a problem running a blocking task on a thread pool. #[derive(Debug, Display, Error)] #[display(fmt = "Blocking thread pool is shut down unexpectedly")] #[non_exhaustive] pub struct BlockingError; impl ResponseError for crate::error::BlockingError {} /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Eq, Display, Error, From)] #[non_exhaustive] pub enum UrlGenerationError { /// Resource not found. #[display(fmt = "Resource not found")] ResourceNotFound, /// Not all URL parameters covered. #[display(fmt = "Not all URL parameters covered")] NotEnoughElements, /// URL parse error. #[display(fmt = "{}", _0)] ParseError(UrlParseError), } impl ResponseError for UrlGenerationError {} /// A set of errors that can occur during parsing urlencoded payloads #[derive(Debug, Display, Error, From)] #[non_exhaustive] pub enum UrlencodedError { /// Can not decode chunked transfer encoding. #[display(fmt = "Can not decode chunked transfer encoding.")] Chunked, /// Payload size is larger than allowed. (default limit: 256kB). #[display( fmt = "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).", size, limit )] Overflow { size: usize, limit: usize }, /// Payload size is now known. #[display(fmt = "Payload size is now known.")] UnknownLength, /// Content type error. #[display(fmt = "Content type error.")] ContentType, /// Parse error. #[display(fmt = "Parse error: {}.", _0)] Parse(FormDeError), /// Encoding error. #[display(fmt = "Encoding error.")] Encoding, /// Serialize error. #[display(fmt = "Serialize error: {}.", _0)] Serialize(FormError), /// Payload error. #[display(fmt = "Error that occur during reading payload: {}.", _0)] Payload(PayloadError), } impl ResponseError for UrlencodedError { fn status_code(&self) -> StatusCode { match self { Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE, Self::UnknownLength => StatusCode::LENGTH_REQUIRED, Self::ContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE, Self::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, } } } /// A set of errors that can occur during parsing json payloads #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum JsonPayloadError { /// Payload size is bigger than allowed & content length header set. (default: 2MB) #[display( fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).", length, limit )] OverflowKnownLength { length: usize, limit: usize }, /// Payload size is bigger than allowed but no content length header set. (default: 2MB) #[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)] Overflow { limit: usize }, /// Content type error #[display(fmt = "Content type error")] ContentType, /// Deserialize error #[display(fmt = "Json deserialize error: {}", _0)] Deserialize(JsonError), /// Serialize error #[display(fmt = "Json serialize error: {}", _0)] Serialize(JsonError), /// Payload error #[display(fmt = "Error that occur during reading payload: {}", _0)] Payload(PayloadError), } impl From for JsonPayloadError { fn from(err: PayloadError) -> Self { Self::Payload(err) } } impl ResponseError for JsonPayloadError { fn status_code(&self) -> StatusCode { match self { Self::OverflowKnownLength { length: _, limit: _, } => StatusCode::PAYLOAD_TOO_LARGE, Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE, Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, } } } /// A set of errors that can occur during parsing request paths #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum PathError { /// Deserialize error #[display(fmt = "Path deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } /// Return `BadRequest` for `PathError` impl ResponseError for PathError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } /// A set of errors that can occur during parsing query strings. #[derive(Debug, Display, Error, From)] #[non_exhaustive] pub enum QueryPayloadError { /// Query deserialize error. #[display(fmt = "Query deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } impl ResponseError for QueryPayloadError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } /// Error type returned when reading body as lines. #[derive(Debug, Display, Error, From)] #[non_exhaustive] pub enum ReadlinesError { #[display(fmt = "Encoding error")] /// Payload size is bigger than allowed. (default: 256kB) EncodingError, /// Payload error. #[display(fmt = "Error that occur during reading payload: {}", _0)] Payload(PayloadError), /// Line limit exceeded. #[display(fmt = "Line limit exceeded")] LimitOverflow, /// ContentType error. #[display(fmt = "Content-type error")] ContentTypeError(ContentTypeError), } impl ResponseError for ReadlinesError { fn status_code(&self) -> StatusCode { match *self { ReadlinesError::LimitOverflow => StatusCode::PAYLOAD_TOO_LARGE, _ => StatusCode::BAD_REQUEST, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_urlencoded_error() { let resp = UrlencodedError::Overflow { size: 0, limit: 0 }.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); let resp = UrlencodedError::UnknownLength.error_response(); assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); let resp = UrlencodedError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); } #[test] fn test_json_payload_error() { let resp = JsonPayloadError::OverflowKnownLength { length: 0, limit: 0, } .error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); let resp = JsonPayloadError::Overflow { limit: 0 }.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); let resp = JsonPayloadError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_query_payload_error() { let resp = QueryPayloadError::Deserialize( serde_urlencoded::from_str::("bad query").unwrap_err(), ) .error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_readlines_error() { let resp = ReadlinesError::LimitOverflow.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); let resp = ReadlinesError::EncodingError.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } actix-web-4.9.0/src/error/response_error.rs000064400000000000000000000104111046102023000170530ustar 00000000000000//! `ResponseError` trait and foreign impls. use std::{ convert::Infallible, error::Error as StdError, fmt, io::{self, Write as _}, }; use actix_http::Response; use bytes::BytesMut; use crate::{ body::BoxBody, error::{downcast_dyn, downcast_get_type_id}, helpers, http::{ header::{self, TryIntoHeaderValue}, StatusCode, }, HttpResponse, }; /// Errors that can generate responses. // TODO: flesh out documentation pub trait ResponseError: fmt::Debug + fmt::Display { /// Returns appropriate status code for error. /// /// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is /// also implemented and does not call `self.status_code()`, then this will not be used. fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } /// Creates full response for error. /// /// By default, the generated response uses a 500 Internal Server Error status code, a /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. fn error_response(&self) -> HttpResponse { let mut res = HttpResponse::new(self.status_code()); let mut buf = BytesMut::new(); let _ = write!(helpers::MutWriter(&mut buf), "{}", self); let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); res.headers_mut().insert(header::CONTENT_TYPE, mime); res.set_body(BoxBody::new(buf)) } downcast_get_type_id!(); } downcast_dyn!(ResponseError); impl ResponseError for Box {} impl ResponseError for Infallible { fn status_code(&self) -> StatusCode { match *self {} } fn error_response(&self) -> HttpResponse { match *self {} } } #[cfg(feature = "openssl")] impl ResponseError for actix_tls::accept::openssl::reexports::Error {} impl ResponseError for serde::de::value::Error { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } impl ResponseError for serde_json::Error {} impl ResponseError for serde_urlencoded::ser::Error {} impl ResponseError for std::str::Utf8Error { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } impl ResponseError for std::io::Error { fn status_code(&self) -> StatusCode { match self.kind() { io::ErrorKind::NotFound => StatusCode::NOT_FOUND, io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, _ => StatusCode::INTERNAL_SERVER_ERROR, } } } impl ResponseError for actix_http::error::HttpError {} impl ResponseError for actix_http::Error { fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } fn error_response(&self) -> HttpResponse { HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body() } } impl ResponseError for actix_http::header::InvalidHeaderValue { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } impl ResponseError for actix_http::error::ParseError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } impl ResponseError for actix_http::error::PayloadError { fn status_code(&self) -> StatusCode { match *self { actix_http::error::PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, _ => StatusCode::BAD_REQUEST, } } } impl ResponseError for actix_http::ws::ProtocolError {} impl ResponseError for actix_http::error::ContentTypeError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } impl ResponseError for actix_http::ws::HandshakeError { fn error_response(&self) -> HttpResponse { Response::from(self).map_into_boxed_body().into() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_casting() { use actix_http::error::{ContentTypeError, PayloadError}; let err = PayloadError::Overflow; let resp_err: &dyn ResponseError = &err; let err = resp_err.downcast_ref::().unwrap(); assert_eq!(err.to_string(), "payload reached size limit"); let not_err = resp_err.downcast_ref::(); assert!(not_err.is_none()); } } actix-web-4.9.0/src/extract.rs000064400000000000000000000423731046102023000143410ustar 00000000000000//! Request extractors use std::{ convert::Infallible, future::Future, marker::PhantomData, pin::Pin, task::{Context, Poll}, }; use actix_http::{Method, Uri}; use actix_utils::future::{ok, Ready}; use futures_core::ready; use pin_project_lite::pin_project; use crate::{dev::Payload, Error, HttpRequest}; /// A type that implements [`FromRequest`] is called an **extractor** and can extract data from /// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`]. /// /// Check out [`ServiceRequest::extract`](crate::dev::ServiceRequest::extract) if you want to /// leverage extractors when implementing middlewares. /// /// # Configuration /// An extractor can be customized by injecting the corresponding configuration with one of: /// - [`App::app_data()`][crate::App::app_data] /// - [`Scope::app_data()`][crate::Scope::app_data] /// - [`Resource::app_data()`][crate::Resource::app_data] /// /// Here are some built-in extractors and their corresponding configuration. /// Please refer to the respective documentation for details. /// /// | Extractor | Configuration | /// |-------------|-------------------| /// | [`Header`] | _None_ | /// | [`Path`] | [`PathConfig`] | /// | [`Json`] | [`JsonConfig`] | /// | [`Form`] | [`FormConfig`] | /// | [`Query`] | [`QueryConfig`] | /// | [`Bytes`] | [`PayloadConfig`] | /// | [`String`] | [`PayloadConfig`] | /// | [`Payload`] | [`PayloadConfig`] | /// /// # Implementing An Extractor /// To reduce duplicate code in handlers where extracting certain parts of a request has a common /// structure, you can implement `FromRequest` for your own types. /// /// Note that the request payload can only be consumed by one extractor. /// /// [`Header`]: crate::web::Header /// [`Json`]: crate::web::Json /// [`JsonConfig`]: crate::web::JsonConfig /// [`Form`]: crate::web::Form /// [`FormConfig`]: crate::web::FormConfig /// [`Path`]: crate::web::Path /// [`PathConfig`]: crate::web::PathConfig /// [`Query`]: crate::web::Query /// [`QueryConfig`]: crate::web::QueryConfig /// [`Payload`]: crate::web::Payload /// [`PayloadConfig`]: crate::web::PayloadConfig /// [`String`]: FromRequest#impl-FromRequest-for-String /// [`Bytes`]: crate::web::Bytes#impl-FromRequest /// [`Either`]: crate::web::Either #[doc(alias = "extract", alias = "extractor")] pub trait FromRequest: Sized { /// The associated error which can be returned. type Error: Into; /// Future that resolves to a `Self`. /// /// To use an async function or block, the futures must be boxed. The following snippet will be /// common when creating async/await extractors (that do not consume the body). /// /// ```ignore /// type Future = Pin>>>; /// // or /// type Future = futures_util::future::LocalBoxFuture<'static, Result>; /// /// fn from_request(req: HttpRequest, ...) -> Self::Future { /// let req = req.clone(); /// Box::pin(async move { /// ... /// }) /// } /// ``` type Future: Future>; /// Create a `Self` from request parts asynchronously. fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future; /// Create a `Self` from request head asynchronously. /// /// This method is short for `T::from_request(req, &mut Payload::None)`. fn extract(req: &HttpRequest) -> Self::Future { Self::from_request(req, &mut Payload::None) } } /// Optionally extract from the request. /// /// If the inner `T::from_request` returns an error, handler will receive `None` instead. /// /// # Examples /// ``` /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; /// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] /// struct Thing { /// name: String /// } /// /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { /// ok(Thing { name: "thingy".into() }) /// } else { /// err(ErrorBadRequest("no luck")) /// } /// /// } /// } /// /// /// extract `Thing` from request /// async fn index(supplied_thing: Option) -> String { /// match supplied_thing { /// // Puns not intended /// Some(thing) => format!("Got something: {:?}", thing), /// None => format!("No thing!") /// } /// } /// /// let app = App::new().service( /// web::resource("/users/:first").route( /// web::post().to(index)) /// ); /// ``` impl FromRequest for Option where T: FromRequest, { type Error = Infallible; type Future = FromRequestOptFuture; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { FromRequestOptFuture { fut: T::from_request(req, payload), } } } pin_project! { pub struct FromRequestOptFuture { #[pin] fut: Fut, } } impl Future for FromRequestOptFuture where Fut: Future>, E: Into, { type Output = Result, Infallible>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let res = ready!(this.fut.poll(cx)); match res { Ok(t) => Poll::Ready(Ok(Some(t))), Err(err) => { log::debug!("Error for Option extractor: {}", err.into()); Poll::Ready(Ok(None)) } } } } /// Extract from the request, passing error type through to handler. /// /// If the inner `T::from_request` returns an error, allow handler to receive the error rather than /// immediately returning an error response. /// /// # Examples /// ``` /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; /// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] /// struct Thing { /// name: String /// } /// /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { /// ok(Thing { name: "thingy".into() }) /// } else { /// err(ErrorBadRequest("no luck")) /// } /// } /// } /// /// /// extract `Thing` from request /// async fn index(supplied_thing: Result) -> String { /// match supplied_thing { /// Ok(thing) => format!("Got thing: {thing:?}"), /// Err(err) => format!("Error extracting thing: {err}"), /// } /// } /// /// let app = App::new().service( /// web::resource("/users/:first").route(web::post().to(index)) /// ); /// ``` impl FromRequest for Result where T: FromRequest, T::Error: Into, { type Error = Infallible; type Future = FromRequestResFuture; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { FromRequestResFuture { fut: T::from_request(req, payload), _phantom: PhantomData, } } } pin_project! { pub struct FromRequestResFuture { #[pin] fut: Fut, _phantom: PhantomData, } } impl Future for FromRequestResFuture where Fut: Future>, Ei: Into, { type Output = Result, Infallible>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let res = ready!(this.fut.poll(cx)); Poll::Ready(Ok(res.map_err(Into::into))) } } /// Extract the request's URI. /// /// # Examples /// ``` /// use actix_web::{http::Uri, web, App, Responder}; /// /// async fn handler(uri: Uri) -> impl Responder { /// format!("Requested path: {}", uri.path()) /// } /// /// let app = App::new().default_service(web::to(handler)); /// ``` impl FromRequest for Uri { type Error = Infallible; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.uri().clone()) } } /// Extract the request's method. /// /// # Examples /// ``` /// use actix_web::{http::Method, web, App, Responder}; /// /// async fn handler(method: Method) -> impl Responder { /// format!("Request method: {}", method) /// } /// /// let app = App::new().default_service(web::to(handler)); /// ``` impl FromRequest for Method { type Error = Infallible; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.method().clone()) } } #[doc(hidden)] #[allow(non_snake_case)] mod tuple_from_req { use super::*; macro_rules! tuple_from_req { ($fut: ident; $($T: ident),*) => { /// FromRequest implementation for tuple #[allow(unused_parens)] impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) { type Error = Error; type Future = $fut<$($T),+>; fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { $fut { $( $T: ExtractFuture::Future { fut: $T::from_request(req, payload) }, )+ } } } pin_project! { pub struct $fut<$($T: FromRequest),+> { $( #[pin] $T: ExtractFuture<$T::Future, $T>, )+ } } impl<$($T: FromRequest),+> Future for $fut<$($T),+> { type Output = Result<($($T,)+), Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.project(); let mut ready = true; $( match this.$T.as_mut().project() { ExtractProj::Future { fut } => match fut.poll(cx) { Poll::Ready(Ok(output)) => { let _ = this.$T.as_mut().project_replace(ExtractFuture::Done { output }); }, Poll::Ready(Err(err)) => return Poll::Ready(Err(err.into())), Poll::Pending => ready = false, }, ExtractProj::Done { .. } => {}, ExtractProj::Empty => unreachable!("FromRequest polled after finished"), } )+ if ready { Poll::Ready(Ok( ($( match this.$T.project_replace(ExtractFuture::Empty) { ExtractReplaceProj::Done { output } => output, _ => unreachable!("FromRequest polled after finished"), }, )+) )) } else { Poll::Pending } } } }; } pin_project! { #[project = ExtractProj] #[project_replace = ExtractReplaceProj] enum ExtractFuture { Future { #[pin] fut: Fut }, Done { output: Res, }, Empty } } impl FromRequest for () { type Error = Infallible; type Future = Ready>; fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { ok(()) } } tuple_from_req! { TupleFromRequest1; A } tuple_from_req! { TupleFromRequest2; A, B } tuple_from_req! { TupleFromRequest3; A, B, C } tuple_from_req! { TupleFromRequest4; A, B, C, D } tuple_from_req! { TupleFromRequest5; A, B, C, D, E } tuple_from_req! { TupleFromRequest6; A, B, C, D, E, F } tuple_from_req! { TupleFromRequest7; A, B, C, D, E, F, G } tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H } tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I } tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K } tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L } tuple_from_req! { TupleFromRequest13; A, B, C, D, E, F, G, H, I, J, K, L, M } tuple_from_req! { TupleFromRequest14; A, B, C, D, E, F, G, H, I, J, K, L, M, N } tuple_from_req! { TupleFromRequest15; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O } tuple_from_req! { TupleFromRequest16; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P } } #[cfg(test)] mod tests { use actix_http::header; use bytes::Bytes; use serde::Deserialize; use super::*; use crate::{ test::TestRequest, types::{Form, FormConfig}, }; #[derive(Deserialize, Debug, PartialEq)] struct Info { hello: String, } #[actix_rt::test] async fn test_option() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .data(FormConfig::default().limit(4096)) .to_http_parts(); let r = Option::>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(r, None); let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((header::CONTENT_LENGTH, "9")) .set_payload(Bytes::from_static(b"hello=world")) .to_http_parts(); let r = Option::>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!( r, Some(Form(Info { hello: "world".into() })) ); let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((header::CONTENT_LENGTH, "9")) .set_payload(Bytes::from_static(b"bye=world")) .to_http_parts(); let r = Option::>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(r, None); } #[actix_rt::test] async fn test_result() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((header::CONTENT_LENGTH, "11")) .set_payload(Bytes::from_static(b"hello=world")) .to_http_parts(); let r = Result::, Error>::from_request(&req, &mut pl) .await .unwrap() .unwrap(); assert_eq!( r, Form(Info { hello: "world".into() }) ); let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((header::CONTENT_LENGTH, 9)) .set_payload(Bytes::from_static(b"bye=world")) .to_http_parts(); struct MyError; impl From for MyError { fn from(_: Error) -> Self { Self } } let r = Result::, MyError>::from_request(&req, &mut pl) .await .unwrap(); assert!(r.is_err()); } #[actix_rt::test] async fn test_uri() { let req = TestRequest::default().uri("/foo/bar").to_http_request(); let uri = Uri::extract(&req).await.unwrap(); assert_eq!(uri.path(), "/foo/bar"); } #[actix_rt::test] async fn test_method() { let req = TestRequest::default().method(Method::GET).to_http_request(); let method = Method::extract(&req).await.unwrap(); assert_eq!(method, Method::GET); } #[actix_rt::test] async fn test_concurrent() { let (req, mut pl) = TestRequest::default() .uri("/foo/bar") .method(Method::GET) .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((header::CONTENT_LENGTH, "11")) .set_payload(Bytes::from_static(b"hello=world")) .to_http_parts(); let (method, uri, form) = <(Method, Uri, Form)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(method, Method::GET); assert_eq!(uri.path(), "/foo/bar"); assert_eq!( form, Form(Info { hello: "world".into() }) ); } } actix-web-4.9.0/src/guard/acceptable.rs000064400000000000000000000057641046102023000160570ustar 00000000000000use super::{Guard, GuardContext}; use crate::http::header::Accept; /// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type. /// /// An exception is that matching `*/*` must be explicitly enabled because most browsers send this /// as part of their `Accept` header for almost every request. /// /// # Examples /// ``` /// use actix_web::{guard::Acceptable, web, HttpResponse}; /// /// web::resource("/images") /// .guard(Acceptable::new(mime::IMAGE_STAR)) /// .default_service(web::to(|| async { /// HttpResponse::Ok().body("only called when images responses are acceptable") /// })); /// ``` #[derive(Debug, Clone)] pub struct Acceptable { mime: mime::Mime, /// Whether to match `*/*` mime type. /// /// Defaults to false because it's not very useful otherwise. match_star_star: bool, } impl Acceptable { /// Constructs new `Acceptable` guard with the given `mime` type/pattern. pub fn new(mime: mime::Mime) -> Self { Self { mime, match_star_star: false, } } /// Allows `*/*` in the `Accept` header to pass the guard check. pub fn match_star_star(mut self) -> Self { self.match_star_star = true; self } } impl Guard for Acceptable { fn check(&self, ctx: &GuardContext<'_>) -> bool { let accept = match ctx.header::() { Some(hdr) => hdr, None => return false, }; let target_type = self.mime.type_(); let target_subtype = self.mime.subtype(); for mime in accept.0.into_iter().map(|q| q.item) { return match (mime.type_(), mime.subtype()) { (typ, subtype) if typ == target_type && subtype == target_subtype => true, (typ, mime::STAR) if typ == target_type => true, (mime::STAR, mime::STAR) if self.match_star_star => true, _ => continue, }; } false } } #[cfg(test)] mod tests { use super::*; use crate::{http::header, test::TestRequest}; #[test] fn test_acceptable() { let req = TestRequest::default().to_srv_request(); assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); let req = TestRequest::default() .insert_header((header::ACCEPT, "application/json")) .to_srv_request(); assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); let req = TestRequest::default() .insert_header((header::ACCEPT, "text/html, application/json")) .to_srv_request(); assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); } #[test] fn test_acceptable_star() { let req = TestRequest::default() .insert_header((header::ACCEPT, "text/html, */*;q=0.8")) .to_srv_request(); assert!(Acceptable::new(mime::APPLICATION_JSON) .match_star_star() .check(&req.guard_ctx())); } } actix-web-4.9.0/src/guard/host.rs000064400000000000000000000147521046102023000147460ustar 00000000000000use actix_http::{header, uri::Uri, RequestHead}; use super::{Guard, GuardContext}; /// Creates a guard that matches requests targeting a specific host. /// /// # Matching Host /// This guard will: /// - match against the `Host` header, if present; /// - fall-back to matching against the request target's host, if present; /// - return false if host cannot be determined; /// /// # Matching Scheme /// Optionally, this guard can match against the host's scheme. Set the scheme for matching using /// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent /// the guard from matching successfully. /// /// # Examples /// The `Host` guard can be used to set up a form of [virtual hosting] within a single app. /// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard /// definitions they become safe to use in this way. Without these host guards, only routes under /// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` /// and `localhost` as the `Host` guards. /// ``` /// use actix_web::{web, http::Method, guard, App, HttpResponse}; /// /// App::new() /// .service( /// web::scope("") /// .guard(guard::Host("www.rust-lang.org")) /// .default_service(web::to(|| async { /// HttpResponse::Ok().body("marketing site") /// })), /// ) /// .service( /// web::scope("") /// .guard(guard::Host("play.rust-lang.org")) /// .default_service(web::to(|| async { /// HttpResponse::Ok().body("playground frontend") /// })), /// ); /// ``` /// /// The example below additionally guards on the host URI's scheme. This could allow routing to /// different handlers for `http:` vs `https:` visitors; to redirect, for example. /// ``` /// use actix_web::{web, guard::Host, HttpResponse}; /// /// web::scope("/admin") /// .guard(Host("admin.rust-lang.org").scheme("https")) /// .default_service(web::to(|| async { /// HttpResponse::Ok().body("admin connection is secure") /// })); /// ``` /// /// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting #[allow(non_snake_case)] pub fn Host(host: impl AsRef) -> HostGuard { HostGuard { host: host.as_ref().to_string(), scheme: None, } } fn get_host_uri(req: &RequestHead) -> Option { req.headers .get(header::HOST) .and_then(|host_value| host_value.to_str().ok()) .or_else(|| req.uri.host()) .and_then(|host| host.parse().ok()) } #[doc(hidden)] pub struct HostGuard { host: String, scheme: Option, } impl HostGuard { /// Set request scheme to match pub fn scheme>(mut self, scheme: H) -> HostGuard { self.scheme = Some(scheme.as_ref().to_string()); self } } impl Guard for HostGuard { fn check(&self, ctx: &GuardContext<'_>) -> bool { // parse host URI from header or request target let req_host_uri = match get_host_uri(ctx.head()) { Some(uri) => uri, // no match if host cannot be determined None => return false, }; match req_host_uri.host() { // fall through to scheme checks Some(uri_host) if self.host == uri_host => {} // Either: // - request's host does not match guard's host; // - It was possible that the parsed URI from request target did not contain a host. _ => return false, } if let Some(ref scheme) = self.scheme { if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { return scheme == req_host_uri_scheme; } // TODO: is this the correct behavior? // falls through if scheme cannot be determined } // all conditions passed true } } #[cfg(test)] mod tests { use super::*; use crate::test::TestRequest; #[test] fn host_from_header() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("www.rust-lang.org"), )) .to_srv_request(); let host = Host("www.rust-lang.org"); assert!(host.check(&req.guard_ctx())); let host = Host("www.rust-lang.org").scheme("https"); assert!(host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org"); assert!(!host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org").scheme("https"); assert!(!host.check(&req.guard_ctx())); let host = Host("crates.io"); assert!(!host.check(&req.guard_ctx())); let host = Host("localhost"); assert!(!host.check(&req.guard_ctx())); } #[test] fn host_without_header() { let req = TestRequest::default() .uri("www.rust-lang.org") .to_srv_request(); let host = Host("www.rust-lang.org"); assert!(host.check(&req.guard_ctx())); let host = Host("www.rust-lang.org").scheme("https"); assert!(host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org"); assert!(!host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org").scheme("https"); assert!(!host.check(&req.guard_ctx())); let host = Host("crates.io"); assert!(!host.check(&req.guard_ctx())); let host = Host("localhost"); assert!(!host.check(&req.guard_ctx())); } #[test] fn host_scheme() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("https://www.rust-lang.org"), )) .to_srv_request(); let host = Host("www.rust-lang.org").scheme("https"); assert!(host.check(&req.guard_ctx())); let host = Host("www.rust-lang.org"); assert!(host.check(&req.guard_ctx())); let host = Host("www.rust-lang.org").scheme("http"); assert!(!host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org"); assert!(!host.check(&req.guard_ctx())); let host = Host("blog.rust-lang.org").scheme("https"); assert!(!host.check(&req.guard_ctx())); let host = Host("crates.io").scheme("https"); assert!(!host.check(&req.guard_ctx())); let host = Host("localhost"); assert!(!host.check(&req.guard_ctx())); } } actix-web-4.9.0/src/guard/mod.rs000064400000000000000000000366111046102023000145460ustar 00000000000000//! Route guards. //! //! Guards are used during routing to help select a matching service or handler using some aspect of //! the request; though guards should not be used for path matching since it is a built-in function //! of the Actix Web router. //! //! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services. //! //! Fundamentally, a guard is a predicate function that receives a reference to a request context //! object and returns a boolean; true if the request _should_ be handled by the guarded service //! or handler. This interface is defined by the [`Guard`] trait. //! //! Commonly-used guards are provided in this module as well as a way of creating a guard from a //! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be //! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on //! services multiple times (which might have different combining behavior than you want). //! //! There are shortcuts for routes with method guards in the [`web`](crate::web) module: //! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by //! the following calls are equivalent: //! //! - `web::get()` (recommended form) //! - `web::route().guard(guard::Get())` //! //! Guards can not modify anything about the request. However, it is possible to store extra //! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`]. //! //! Guards can prevent resource definitions from overlapping which, when only considering paths, //! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting. //! //! # Examples //! //! In the following code, the `/guarded` resource has one defined route whose handler will only be //! called if the request method is GET or POST and there is a `x-guarded` request header with value //! equal to `secret`. //! //! ``` //! use actix_web::{web, http::Method, guard, HttpResponse}; //! //! web::resource("/guarded").route( //! web::route() //! .guard(guard::Any(guard::Get()).or(guard::Post())) //! .guard(guard::Header("x-guarded", "secret")) //! .to(|| HttpResponse::Ok()) //! ); //! ``` //! //! [`Scope`]: crate::Scope::guard() //! [`Resource`]: crate::Resource::guard() //! [`Route`]: crate::Route::guard() use std::{ cell::{Ref, RefMut}, rc::Rc, }; use actix_http::{header, Extensions, Method as HttpMethod, RequestHead}; use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; mod acceptable; mod host; pub use self::{ acceptable::Acceptable, host::{Host, HostGuard}, }; /// Provides access to request parts that are useful during routing. #[derive(Debug)] pub struct GuardContext<'a> { pub(crate) req: &'a ServiceRequest, } impl<'a> GuardContext<'a> { /// Returns reference to the request head. #[inline] pub fn head(&self) -> &RequestHead { self.req.head() } /// Returns reference to the request-local data/extensions container. #[inline] pub fn req_data(&self) -> Ref<'a, Extensions> { self.req.extensions() } /// Returns mutable reference to the request-local data/extensions container. #[inline] pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { self.req.extensions_mut() } /// Extracts a typed header from the request. /// /// Returns `None` if parsing `H` fails. /// /// # Examples /// ``` /// use actix_web::{guard::fn_guard, http::header}; /// /// let image_accept_guard = fn_guard(|ctx| { /// match ctx.header::() { /// Some(hdr) => hdr.preference() == "image/*", /// None => false, /// } /// }); /// ``` #[inline] pub fn header(&self) -> Option { H::parse(self.req).ok() } /// Counterpart to [HttpRequest::app_data](crate::HttpRequest::app_data). #[inline] pub fn app_data(&self) -> Option<&T> { self.req.app_data() } } /// Interface for routing guards. /// /// See [module level documentation](self) for more. pub trait Guard { /// Returns true if predicate condition is met for a given request. fn check(&self, ctx: &GuardContext<'_>) -> bool; } impl Guard for Rc { fn check(&self, ctx: &GuardContext<'_>) -> bool { (**self).check(ctx) } } /// Creates a guard using the given function. /// /// # Examples /// ``` /// use actix_web::{guard, web, HttpResponse}; /// /// web::route() /// .guard(guard::fn_guard(|ctx| { /// ctx.head().headers().contains_key("content-type") /// })) /// .to(|| HttpResponse::Ok()); /// ``` pub fn fn_guard(f: F) -> impl Guard where F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } struct FnGuard) -> bool>(F); impl Guard for FnGuard where F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self.0)(ctx) } } impl Guard for F where F: Fn(&GuardContext<'_>) -> bool, { fn check(&self, ctx: &GuardContext<'_>) -> bool { (self)(ctx) } } /// Creates a guard that matches if any added guards match. /// /// # Examples /// The handler below will be called for either request method `GET` or `POST`. /// ``` /// use actix_web::{web, guard, HttpResponse}; /// /// web::route() /// .guard( /// guard::Any(guard::Get()) /// .or(guard::Post())) /// .to(|| HttpResponse::Ok()); /// ``` #[allow(non_snake_case)] pub fn Any(guard: F) -> AnyGuard { AnyGuard { guards: vec![Box::new(guard)], } } /// A collection of guards that match if the disjunction of their `check` outcomes is true. /// /// That is, only one contained guard needs to match in order for the aggregate guard to match. /// /// Construct an `AnyGuard` using [`Any`]. pub struct AnyGuard { guards: Vec>, } impl AnyGuard { /// Adds new guard to the collection of guards to check. pub fn or(mut self, guard: F) -> Self { self.guards.push(Box::new(guard)); self } } impl Guard for AnyGuard { #[inline] fn check(&self, ctx: &GuardContext<'_>) -> bool { for guard in &self.guards { if guard.check(ctx) { return true; } } false } } /// Creates a guard that matches if all added guards match. /// /// # Examples /// The handler below will only be called if the request method is `GET` **and** the specified /// header name and value match exactly. /// ``` /// use actix_web::{guard, web, HttpResponse}; /// /// web::route() /// .guard( /// guard::All(guard::Get()) /// .and(guard::Header("accept", "text/plain")) /// ) /// .to(|| HttpResponse::Ok()); /// ``` #[allow(non_snake_case)] pub fn All(guard: F) -> AllGuard { AllGuard { guards: vec![Box::new(guard)], } } /// A collection of guards that match if the conjunction of their `check` outcomes is true. /// /// That is, **all** contained guard needs to match in order for the aggregate guard to match. /// /// Construct an `AllGuard` using [`All`]. pub struct AllGuard { guards: Vec>, } impl AllGuard { /// Adds new guard to the collection of guards to check. pub fn and(mut self, guard: F) -> Self { self.guards.push(Box::new(guard)); self } } impl Guard for AllGuard { #[inline] fn check(&self, ctx: &GuardContext<'_>) -> bool { for guard in &self.guards { if !guard.check(ctx) { return false; } } true } } /// Wraps a guard and inverts the outcome of its `Guard` implementation. /// /// # Examples /// The handler below will be called for any request method apart from `GET`. /// ``` /// use actix_web::{guard, web, HttpResponse}; /// /// web::route() /// .guard(guard::Not(guard::Get())) /// .to(|| HttpResponse::Ok()); /// ``` pub struct Not(pub G); impl Guard for Not { #[inline] fn check(&self, ctx: &GuardContext<'_>) -> bool { !self.0.check(ctx) } } /// Creates a guard that matches a specified HTTP method. #[allow(non_snake_case)] pub fn Method(method: HttpMethod) -> impl Guard { MethodGuard(method) } #[derive(Debug, Clone)] pub(crate) struct RegisteredMethods(pub(crate) Vec); /// HTTP method guard. #[derive(Debug)] pub(crate) struct MethodGuard(HttpMethod); impl Guard for MethodGuard { fn check(&self, ctx: &GuardContext<'_>) -> bool { let registered = ctx.req_data_mut().remove::(); if let Some(mut methods) = registered { methods.0.push(self.0.clone()); ctx.req_data_mut().insert(methods); } else { ctx.req_data_mut() .insert(RegisteredMethods(vec![self.0.clone()])); } ctx.head().method == self.0 } } macro_rules! method_guard { ($method_fn:ident, $method_const:ident) => { #[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")] /// /// # Examples #[doc = concat!("The route in this example will only respond to `", stringify!($method_const), "` requests.")] /// ``` /// use actix_web::{guard, web, HttpResponse}; /// /// web::route() #[doc = concat!(" .guard(guard::", stringify!($method_fn), "())")] /// .to(|| HttpResponse::Ok()); /// ``` #[allow(non_snake_case)] pub fn $method_fn() -> impl Guard { MethodGuard(HttpMethod::$method_const) } }; } method_guard!(Get, GET); method_guard!(Post, POST); method_guard!(Put, PUT); method_guard!(Delete, DELETE); method_guard!(Head, HEAD); method_guard!(Options, OPTIONS); method_guard!(Connect, CONNECT); method_guard!(Patch, PATCH); method_guard!(Trace, TRACE); /// Creates a guard that matches if request contains given header name and value. /// /// # Examples /// The handler below will be called when the request contains an `x-guarded` header with value /// equal to `secret`. /// ``` /// use actix_web::{guard, web, HttpResponse}; /// /// web::route() /// .guard(guard::Header("x-guarded", "secret")) /// .to(|| HttpResponse::Ok()); /// ``` #[allow(non_snake_case)] pub fn Header(name: &'static str, value: &'static str) -> impl Guard { HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } struct HeaderGuard(header::HeaderName, header::HeaderValue); impl Guard for HeaderGuard { fn check(&self, ctx: &GuardContext<'_>) -> bool { if let Some(val) = ctx.head().headers.get(&self.0) { return val == self.1; } false } } #[cfg(test)] mod tests { use actix_http::Method; use super::*; use crate::test::TestRequest; #[test] fn header_match() { let req = TestRequest::default() .insert_header((header::TRANSFER_ENCODING, "chunked")) .to_srv_request(); let hdr = Header("transfer-encoding", "chunked"); assert!(hdr.check(&req.guard_ctx())); let hdr = Header("transfer-encoding", "other"); assert!(!hdr.check(&req.guard_ctx())); let hdr = Header("content-type", "chunked"); assert!(!hdr.check(&req.guard_ctx())); let hdr = Header("content-type", "other"); assert!(!hdr.check(&req.guard_ctx())); } #[test] fn method_guards() { let get_req = TestRequest::get().to_srv_request(); let post_req = TestRequest::post().to_srv_request(); assert!(Get().check(&get_req.guard_ctx())); assert!(!Get().check(&post_req.guard_ctx())); assert!(Post().check(&post_req.guard_ctx())); assert!(!Post().check(&get_req.guard_ctx())); let req = TestRequest::put().to_srv_request(); assert!(Put().check(&req.guard_ctx())); assert!(!Put().check(&get_req.guard_ctx())); let req = TestRequest::patch().to_srv_request(); assert!(Patch().check(&req.guard_ctx())); assert!(!Patch().check(&get_req.guard_ctx())); let r = TestRequest::delete().to_srv_request(); assert!(Delete().check(&r.guard_ctx())); assert!(!Delete().check(&get_req.guard_ctx())); let req = TestRequest::default().method(Method::HEAD).to_srv_request(); assert!(Head().check(&req.guard_ctx())); assert!(!Head().check(&get_req.guard_ctx())); let req = TestRequest::default() .method(Method::OPTIONS) .to_srv_request(); assert!(Options().check(&req.guard_ctx())); assert!(!Options().check(&get_req.guard_ctx())); let req = TestRequest::default() .method(Method::CONNECT) .to_srv_request(); assert!(Connect().check(&req.guard_ctx())); assert!(!Connect().check(&get_req.guard_ctx())); let req = TestRequest::default() .method(Method::TRACE) .to_srv_request(); assert!(Trace().check(&req.guard_ctx())); assert!(!Trace().check(&get_req.guard_ctx())); } #[test] fn aggregate_any() { let req = TestRequest::default() .method(Method::TRACE) .to_srv_request(); assert!(Any(Trace()).check(&req.guard_ctx())); assert!(Any(Trace()).or(Get()).check(&req.guard_ctx())); assert!(!Any(Get()).or(Get()).check(&req.guard_ctx())); } #[test] fn aggregate_all() { let req = TestRequest::default() .method(Method::TRACE) .to_srv_request(); assert!(All(Trace()).check(&req.guard_ctx())); assert!(All(Trace()).and(Trace()).check(&req.guard_ctx())); assert!(!All(Trace()).and(Get()).check(&req.guard_ctx())); } #[test] fn nested_not() { let req = TestRequest::default().to_srv_request(); let get = Get(); assert!(get.check(&req.guard_ctx())); let not_get = Not(get); assert!(!not_get.check(&req.guard_ctx())); let not_not_get = Not(not_get); assert!(not_not_get.check(&req.guard_ctx())); } #[test] fn function_guard() { let domain = "rust-lang.org".to_owned(); let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain)); let req = TestRequest::default() .uri("blog.rust-lang.org") .to_srv_request(); assert!(guard.check(&req.guard_ctx())); let req = TestRequest::default().uri("crates.io").to_srv_request(); assert!(!guard.check(&req.guard_ctx())); } #[test] fn mega_nesting() { let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx)); let req = TestRequest::default().to_srv_request(); assert!(!guard.check(&req.guard_ctx())); let req = TestRequest::default() .method(Method::TRACE) .to_srv_request(); assert!(guard.check(&req.guard_ctx())); } #[test] fn app_data() { const TEST_VALUE: u32 = 42; let guard = fn_guard(|ctx| dbg!(ctx.app_data::()) == Some(&TEST_VALUE)); let req = TestRequest::default().app_data(TEST_VALUE).to_srv_request(); assert!(guard.check(&req.guard_ctx())); let req = TestRequest::default() .app_data(TEST_VALUE * 2) .to_srv_request(); assert!(!guard.check(&req.guard_ctx())); } } actix-web-4.9.0/src/handler.rs000064400000000000000000000143471046102023000143040ustar 00000000000000use std::future::Future; use actix_service::{boxed, fn_service}; use crate::{ service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, FromRequest, HttpResponse, Responder, }; /// The interface for request handlers. /// /// # What Is A Request Handler /// /// In short, a handler is just an async function that receives request-based arguments, in any /// order, and returns something that can be converted to a response. /// /// In particular, a request handler has three requirements: /// /// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. The function parameters (up to 12) implement [`FromRequest`]; /// 1. The async function (or future) resolves to a type that can be converted into an /// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// /// /// # Compiler Errors /// /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not /// fulfill the _first_ of the above requirements. (It could also mean that you're attempting to use /// a macro-routed handler in a manual routing context like `web::get().to(handler)`, which is not /// supported). Breaking the other requirements manifests as errors on implementing [`FromRequest`] /// and [`Responder`], respectively. /// /// # How Do Handlers Receive Variable Numbers Of Arguments /// /// Rest assured there is no macro magic here; it's just traits. /// /// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). /// /// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way /// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args` /// type parameter for this trait). /// /// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the /// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on /// that tuple, and the `Handler::call` implementation for that particular function arity /// destructures the tuple into its component types and calls your handler function with them. /// /// In pseudo-code the process looks something like this: /// /// ```ignore /// async fn my_handler(body: String, state: web::Data) -> impl Responder { /// ... /// } /// /// // the function params above described as a tuple, names do not matter, only position /// type InferredMyHandlerArgs = (String, web::Data); /// /// // create tuple of arguments to be passed to handler /// let args = InferredMyHandlerArgs::from_request(&request, &payload).await; /// /// // call handler with argument tuple /// let response = Handler::call(&my_handler, args).await; /// /// // which is effectively... /// /// let (body, state) = args; /// let response = my_handler(body, state).await; /// ``` /// /// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the /// bounds of the handler call after argument extraction: /// ```ignore /// impl Handler<(Arg1, Arg2)> for Func /// where /// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static, /// Fut: Future, /// { /// type Output = Fut::Output; /// type Future = Fut; /// /// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future { /// (self)(arg1, arg2) /// } /// } /// ``` /// /// [arity]: https://en.wikipedia.org/wiki/Arity /// [`from_request`]: FromRequest::from_request pub trait Handler: Clone + 'static { type Output; type Future: Future; fn call(&self, args: Args) -> Self::Future; } pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where F: Handler, Args: FromRequest, F::Output: Responder, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); async move { let (req, mut payload) = req.into_parts(); let res = match Args::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), Ok(data) => handler .call(data) .await .respond_to(&req) .map_into_boxed_body(), }; Ok(ServiceResponse::new(req, res)) } })) } /// Generates a [`Handler`] trait impl for N-ary functions where N is specified with a sequence of /// space separated type parameters. /// /// # Examples /// ```ignore /// factory_tuple! {} // implements Handler for types: fn() -> R /// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { impl Handler<($($param,)*)> for Func where Func: Fn($($param),*) -> Fut + Clone + 'static, Fut: Future, { type Output = Fut::Output; type Future = Fut; #[inline] #[allow(non_snake_case)] fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future { (self)($($param,)*) } } }); factory_tuple! {} factory_tuple! { A } factory_tuple! { A B } factory_tuple! { A B C } factory_tuple! { A B C D } factory_tuple! { A B C D E } factory_tuple! { A B C D E F } factory_tuple! { A B C D E F G } factory_tuple! { A B C D E F G H } factory_tuple! { A B C D E F G H I } factory_tuple! { A B C D E F G H I J } factory_tuple! { A B C D E F G H I J K } factory_tuple! { A B C D E F G H I J K L } factory_tuple! { A B C D E F G H I J K L M } factory_tuple! { A B C D E F G H I J K L M N } factory_tuple! { A B C D E F G H I J K L M N O } factory_tuple! { A B C D E F G H I J K L M N O P } #[cfg(test)] mod tests { use super::*; fn assert_impl_handler(_: impl Handler) {} #[test] fn arg_number() { async fn handler_min() {} #[rustfmt::skip] #[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits, clippy::let_unit_value)] async fn handler_max( _01: (), _02: (), _03: (), _04: (), _05: (), _06: (), _07: (), _08: (), _09: (), _10: (), _11: (), _12: (), _13: (), _14: (), _15: (), _16: (), ) {} assert_impl_handler(handler_min); assert_impl_handler(handler_max); } } actix-web-4.9.0/src/helpers.rs000064400000000000000000000013211046102023000143150ustar 00000000000000use std::io; use bytes::BufMut; /// An `io::Write`r that only requires mutable reference and assumes that there is space available /// in the buffer for every write operation or that it can be extended implicitly (like /// `bytes::BytesMut`, for example). /// /// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not /// perform a remaining length check before writing. pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); impl<'a, B> io::Write for MutWriter<'a, B> where B: BufMut, { fn write(&mut self, buf: &[u8]) -> io::Result { self.0.put_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } actix-web-4.9.0/src/http/header/accept.rs000064400000000000000000000232341046102023000163300ustar 00000000000000use std::cmp::Ordering; use mime::Mime; use super::{common_header, QualityItem}; use crate::http::header; common_header! { /// `Accept` header, defined in [RFC 7231 §5.3.2]. /// /// The `Accept` header field can be used by user agents to specify /// response media types that are acceptable. Accept header fields can /// be used to indicate that the request is specifically limited to a /// small set of desired types, as in the case of a request for an /// in-line image /// /// # ABNF /// ```plain /// Accept = #( media-range [ accept-params ] ) /// /// media-range = ( "*/*" /// / ( type "/" "*" ) /// / ( type "/" subtype ) /// ) *( OWS ";" OWS parameter ) /// accept-params = weight *( accept-ext ) /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] /// ``` /// /// # Example Values /// * `audio/*; q=0.2, audio/basic` /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{Accept, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// QualityItem::max(mime::TEXT_HTML), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{Accept, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// QualityItem::max(mime::APPLICATION_JSON), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{Accept, QualityItem, q}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// QualityItem::max(mime::TEXT_HTML), /// QualityItem::max("application/xhtml+xml".parse().unwrap()), /// QualityItem::new(mime::TEXT_XML, q(0.9)), /// QualityItem::max("image/webp".parse().unwrap()), /// QualityItem::new(mime::STAR_STAR, q(0.8)), /// ]) /// ); /// ``` /// /// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 (Accept, header::ACCEPT) => (QualityItem)* test_parse_and_format { // Tests from the RFC crate::http::header::common_header_test!( test1, [b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(0.2)), QualityItem::max("audio/basic".parse().unwrap()), ]))); crate::http::header::common_header_test!( test2, [b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN, q(0.5)), QualityItem::max(mime::TEXT_HTML), QualityItem::new( "text/x-dvi".parse().unwrap(), q(0.8)), QualityItem::max("text/x-c".parse().unwrap()), ]))); // Custom tests crate::http::header::common_header_test!( test3, [b"text/plain; charset=utf-8"], Some(Accept(vec![ QualityItem::max(mime::TEXT_PLAIN_UTF_8), ]))); crate::http::header::common_header_test!( test4, [b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)), ]))); #[test] fn test_fuzzing1() { let req = test::TestRequest::default() .insert_header((header::ACCEPT, "chunk#;e")) .finish(); let header = Accept::parse(&req); assert!(header.is_ok()); } } } impl Accept { /// Construct `Accept: */*`. pub fn star() -> Accept { Accept(vec![QualityItem::max(mime::STAR_STAR)]) } /// Construct `Accept: application/json`. pub fn json() -> Accept { Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]) } /// Construct `Accept: text/*`. pub fn text() -> Accept { Accept(vec![QualityItem::max(mime::TEXT_STAR)]) } /// Construct `Accept: image/*`. pub fn image() -> Accept { Accept(vec![QualityItem::max(mime::IMAGE_STAR)]) } /// Construct `Accept: text/html`. pub fn html() -> Accept { Accept(vec![QualityItem::max(mime::TEXT_HTML)]) } // TODO: method for getting best content encoding based on q-factors, available from server side // and if none are acceptable return None /// Extracts the most preferable mime type, accounting for [q-factor weighting]. /// /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained /// list is empty. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn preference(&self) -> Mime { use actix_http::header::Quality; let mut max_item = None; let mut max_pref = Quality::ZERO; // uses manual max lookup loop since we want the first occurrence in the case of same // preference but `Iterator::max_by_key` would give us the last occurrence for pref in &self.0 { // only change if strictly greater // equal items, even while unsorted, still have higher preference if they appear first if pref.quality > max_pref { max_pref = pref.quality; max_item = Some(pref.item.clone()); } } max_item.unwrap_or(mime::STAR_STAR) } /// Returns a sorted list of mime types from highest to lowest preference, accounting for /// [q-factor weighting] and specificity. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn ranked(&self) -> Vec { if self.is_empty() { return vec![]; } let mut types = self.0.clone(); // use stable sort so items with equal q-factor and specificity retain listed order types.sort_by(|a, b| { // sort by q-factor descending b.quality.cmp(&a.quality).then_with(|| { // use specificity rules on mime types with // same q-factor (eg. text/html > text/* > */*) // subtypes are not comparable if main type is star, so return match (a.item.type_(), b.item.type_()) { (mime::STAR, mime::STAR) => return Ordering::Equal, // a is sorted after b (mime::STAR, _) => return Ordering::Greater, // a is sorted before b (_, mime::STAR) => return Ordering::Less, _ => {} } // in both these match expressions, the returned ordering appears // inverted because sort is high-to-low ("descending") precedence match (a.item.subtype(), b.item.subtype()) { (mime::STAR, mime::STAR) => Ordering::Equal, // a is sorted after b (mime::STAR, _) => Ordering::Greater, // a is sorted before b (_, mime::STAR) => Ordering::Less, _ => Ordering::Equal, } }) }); types.into_iter().map(|qitem| qitem.item).collect() } } #[cfg(test)] mod tests { use super::*; use crate::http::header::q; #[test] fn ranking_precedence() { let test = Accept(vec![]); assert!(test.ranked().is_empty()); let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]); assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]); let test = Accept(vec![ QualityItem::max(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), ]); assert_eq!( test.ranked(), vec![ mime::TEXT_HTML, "application/xhtml+xml".parse().unwrap(), "application/xml".parse().unwrap(), mime::STAR_STAR, ] ); let test = Accept(vec![ QualityItem::max(mime::STAR_STAR), QualityItem::max(mime::IMAGE_STAR), QualityItem::max(mime::IMAGE_PNG), ]); assert_eq!( test.ranked(), vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR] ); } #[test] fn preference_selection() { let test = Accept(vec![ QualityItem::max(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), ]); assert_eq!(test.preference(), mime::TEXT_HTML); let test = Accept(vec![ QualityItem::new("video/*".parse().unwrap(), q(0.8)), QualityItem::max(mime::IMAGE_PNG), QualityItem::new(mime::STAR_STAR, q(0.5)), QualityItem::max(mime::IMAGE_SVG), QualityItem::new(mime::IMAGE_STAR, q(0.8)), ]); assert_eq!(test.preference(), mime::IMAGE_PNG); } } actix-web-4.9.0/src/http/header/accept_charset.rs000064400000000000000000000040661046102023000200430ustar 00000000000000use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET}; common_header! { /// `Accept-Charset` header, defined in [RFC 7231 §5.3.3]. /// /// The `Accept-Charset` header field can be sent by a user agent to /// indicate what charsets are acceptable in textual response content. /// This field allows user agents capable of understanding more /// comprehensive or special-purpose charsets to signal that capability /// to an origin server that is capable of representing information in /// those charsets. /// /// # ABNF /// ```plain /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) /// ``` /// /// # Example Values /// * `iso-8859-5, unicode-1-1;q=0.8` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![ /// QualityItem::new(Charset::Us_Ascii, q(0.9)), /// QualityItem::new(Charset::Iso_8859_10, q(0.2)), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) /// ); /// ``` /// /// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3 (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)* test_parse_and_format { // Test case from RFC common_header_test!(test1, [b"iso-8859-5, unicode-1-1;q=0.8"]); } } actix-web-4.9.0/src/http/header/accept_encoding.rs000064400000000000000000000420301046102023000201710ustar 00000000000000use std::collections::HashSet; use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem}; use crate::http::header; common_header! { /// `Accept-Encoding` header, defined /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// /// The `Accept-Encoding` header field can be used by user agents to indicate what response /// content-codings are acceptable in the response. An `identity` token is used as a synonym /// for "no encoding" in order to communicate when no encoding is preferred. /// /// # ABNF /// ```plain /// Accept-Encoding = #( codings [ weight ] ) /// codings = content-coding / "identity" / "*" /// ``` /// /// # Example Values /// * `compress, gzip` /// * `` /// * `*` /// * `compress;q=0.5, gzip;q=1` /// * `gzip;q=1.0, identity; q=0.5, *;q=0` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ /// "gzip".parse().unwrap(), /// "br".parse().unwrap(), /// ]) /// ); /// ``` (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem>)* test_parse_and_format { common_header_test!(no_headers, [b""; 0], Some(AcceptEncoding(vec![]))); common_header_test!(empty_header, [b""; 1], Some(AcceptEncoding(vec![]))); common_header_test!( order_of_appearance, [b"br, gzip"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Specific(Encoding::brotli())), QualityItem::max(Preference::Specific(Encoding::gzip())), ])) ); common_header_test!(any, [b"*"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Any), ]))); // Note: Removed quality 1 from gzip common_header_test!(implicit_quality, [b"gzip, identity; q=0.5, *;q=0"]); // Note: Removed quality 1 from gzip common_header_test!(implicit_quality_out_of_order, [b"compress;q=0.5, gzip"]); common_header_test!( only_gzip_no_identity, [b"gzip, *; q=0"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Specific(Encoding::gzip())), QualityItem::zero(Preference::Any), ])) ); } } impl AcceptEncoding { /// Selects the most acceptable encoding according to client preference and supported types. /// /// The "identity" encoding is not assumed and should be included in the `supported` iterator /// if a non-encoded representation can be selected. /// /// If `None` is returned, this indicates that none of the supported encodings are acceptable to /// the client. The caller should generate a 406 Not Acceptable response (unencoded) that /// includes the server's supported encodings in the body plus a [`Vary`] header. /// /// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary pub fn negotiate<'a>(&self, supported: impl Iterator) -> Option { // 1. If no Accept-Encoding field is in the request, any content-coding is considered // acceptable by the user agent. let supported_set = supported.collect::>(); if supported_set.is_empty() { return None; } if self.0.is_empty() { // though it is not recommended to encode in this case, return identity encoding return Some(Encoding::identity()); } // 2. If the representation has no content-coding, then it is acceptable by default unless // specifically excluded by the Accept-Encoding field stating either "identity;q=0" or // "*;q=0" without a more specific entry for "identity". let acceptable_items = self.ranked_items().collect::>(); let identity_acceptable = is_identity_acceptable(&acceptable_items); let identity_supported = supported_set.contains(&Encoding::identity()); if identity_acceptable && identity_supported && supported_set.len() == 1 { return Some(Encoding::identity()); } // 3. If the representation's content-coding is one of the content-codings listed in the // Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0. // 4. If multiple content-codings are acceptable, then the acceptable content-coding with // the highest non-zero qvalue is preferred. let matched = acceptable_items .into_iter() .filter(|q| q.quality > Quality::ZERO) // search relies on item list being in descending order of quality .find(|q| { let enc = &q.item; matches!(enc, Preference::Specific(enc) if supported_set.contains(enc)) }) .map(|q| q.item); match matched { Some(Preference::Specific(enc)) => Some(enc), _ if identity_acceptable => Some(Encoding::identity()), _ => None, } } /// Extracts the most preferable encoding, accounting for [q-factor weighting]. /// /// If no q-factors are provided, we prefer brotli > zstd > gzip. Note that items without /// q-factors are given the maximum preference value. /// /// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is /// returned, it is recommended to use an un-encoded representation. /// /// If `None` is returned, it means that the client has signalled that no representations /// are acceptable. This should never occur for a well behaved user-agent. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn preference(&self) -> Option> { // empty header indicates no preference if self.0.is_empty() { return Some(Preference::Any); } let mut max_item = None; let mut max_pref = Quality::ZERO; let mut max_rank = 0; // uses manual max lookup loop since we want the first occurrence in the case of same // preference but `Iterator::max_by_key` would give us the last occurrence for pref in &self.0 { // only change if strictly greater // equal items, even while unsorted, still have higher preference if they appear first let rank = encoding_rank(pref); if (pref.quality, rank) > (max_pref, max_rank) { max_pref = pref.quality; max_item = Some(pref.item.clone()); max_rank = rank; } } // Return max_item if any items were above 0 quality... max_item.or_else(|| { // ...or else check for "*" or "identity". We can elide quality checks since // entering this block means all items had "q=0". match self.0.iter().find(|pref| { matches!( pref.item, Preference::Any | Preference::Specific(Encoding::Known(ContentEncoding::Identity)) ) }) { // "identity" or "*" found so no representation is acceptable Some(_) => None, // implicit "identity" is acceptable None => Some(Preference::Specific(Encoding::identity())), } }) } /// Returns a sorted list of encodings from highest to lowest precedence, accounting /// for [q-factor weighting]. /// /// If no q-factors are provided, we prefer brotli > zstd > gzip. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn ranked(&self) -> Vec> { self.ranked_items().map(|q| q.item).collect() } fn ranked_items(&self) -> impl Iterator>> { if self.0.is_empty() { return Vec::new().into_iter(); } let mut types = self.0.clone(); // use stable sort so items with equal q-factor retain listed order types.sort_by(|a, b| { // sort by q-factor descending then server ranking descending b.quality .cmp(&a.quality) .then(encoding_rank(b).cmp(&encoding_rank(a))) }); types.into_iter() } } /// Returns server-defined encoding ranking. fn encoding_rank(qv: &QualityItem>) -> u8 { // ensure that q=0 items are never sorted above identity encoding // invariant: sorting methods calling this fn use first-on-equal approach if qv.quality == Quality::ZERO { return 0; } match qv.item { Preference::Specific(Encoding::Known(ContentEncoding::Brotli)) => 5, Preference::Specific(Encoding::Known(ContentEncoding::Zstd)) => 4, Preference::Specific(Encoding::Known(ContentEncoding::Gzip)) => 3, Preference::Specific(Encoding::Known(ContentEncoding::Deflate)) => 2, Preference::Any => 0, Preference::Specific(Encoding::Known(ContentEncoding::Identity)) => 0, Preference::Specific(Encoding::Known(_)) => 1, Preference::Specific(Encoding::Unknown(_)) => 1, } } /// Returns true if "identity" is an acceptable encoding. /// /// Internal algorithm relies on item list being in descending order of quality. fn is_identity_acceptable(items: &'_ [QualityItem>]) -> bool { if items.is_empty() { return true; } // Loop algorithm depends on items being sorted in descending order of quality. As such, it // is sufficient to return (q > 0) when reaching either an "identity" or "*" item. for q in items { match (q.quality, &q.item) { // occurrence of "identity;q=n"; return true if quality is non-zero (q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => { return q > Quality::ZERO } // occurrence of "*;q=n"; return true if quality is non-zero (q, Preference::Any) => return q > Quality::ZERO, _ => {} } } // implicit acceptable identity true } #[cfg(test)] mod tests { use super::*; use crate::http::header::*; macro_rules! accept_encoding { () => { AcceptEncoding(vec![]) }; ($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) }; } /// Parses an encoding string. fn enc(enc: &str) -> Preference { enc.parse().unwrap() } #[test] fn detect_identity_acceptable() { macro_rules! accept_encoding_ranked { () => { accept_encoding!().ranked_items().collect::>() }; ($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::>() }; } let test = accept_encoding_ranked!(); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "br"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "*;q=0.1"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "identity;q=0.1"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1"); assert!(is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "*;q=0"); assert!(!is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "identity;q=0"); assert!(!is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0"); assert!(!is_identity_acceptable(&test)); let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0"); assert!(!is_identity_acceptable(&test)); } #[test] fn encoding_negotiation() { // no preference let test = accept_encoding!(); assert_eq!(test.negotiate([].iter()), None); let test = accept_encoding!(); assert_eq!( test.negotiate([Encoding::identity()].iter()), Some(Encoding::identity()), ); let test = accept_encoding!("identity;q=0"); assert_eq!(test.negotiate([Encoding::identity()].iter()), None); let test = accept_encoding!("*;q=0"); assert_eq!(test.negotiate([Encoding::identity()].iter()), None); let test = accept_encoding!(); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::identity()), ); let test = accept_encoding!("gzip"); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::gzip()), ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), Some(Encoding::identity()), ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::gzip()), ); let test = accept_encoding!("gzip", "identity;q=0"); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::gzip()), ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), None ); let test = accept_encoding!("gzip", "*;q=0"); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::gzip()), ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), None ); let test = accept_encoding!("gzip", "deflate", "br"); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::gzip()), ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), Some(Encoding::brotli()) ); assert_eq!( test.negotiate([Encoding::deflate(), Encoding::identity()].iter()), Some(Encoding::deflate()) ); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()), Some(Encoding::gzip()) ); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()), Some(Encoding::brotli()) ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()), Some(Encoding::brotli()) ); } #[test] fn ranking_precedence() { let test = accept_encoding!(); assert!(test.ranked().is_empty()); let test = accept_encoding!("gzip"); assert_eq!(test.ranked(), vec![enc("gzip")]); let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0"); assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); let test = accept_encoding!("br", "gzip", "*"); assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); let test = accept_encoding!("gzip", "br", "*"); assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); } #[test] fn preference_selection() { assert_eq!(accept_encoding!().preference(), Some(Preference::Any)); assert_eq!(accept_encoding!("identity;q=0").preference(), None); assert_eq!(accept_encoding!("*;q=0").preference(), None); assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None); assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None); let test = accept_encoding!("*;q=0.5"); assert_eq!(test.preference().unwrap(), enc("*")); let test = accept_encoding!("br;q=0"); assert_eq!(test.preference().unwrap(), enc("identity")); let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500"); assert_eq!(test.preference().unwrap(), enc("gzip")); let test = accept_encoding!("br", "gzip", "*"); assert_eq!(test.preference().unwrap(), enc("br")); let test = accept_encoding!("gzip", "br", "*"); assert_eq!(test.preference().unwrap(), enc("br")); } } actix-web-4.9.0/src/http/header/accept_language.rs000064400000000000000000000171271046102023000201770ustar 00000000000000use language_tags::LanguageTag; use super::{common_header, Preference, Quality, QualityItem}; use crate::http::header; common_header! { /// `Accept-Language` header, defined /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) /// /// The `Accept-Language` header field can be used by user agents to indicate the set of natural /// languages that are preferred in the response. /// /// The `Accept-Language` header is defined in /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). /// /// # ABNF /// ```plain /// Accept-Language = 1#( language-range [ weight ] ) /// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" /// alphanum = ALPHA / DIGIT /// weight = OWS ";" OWS "q=" qvalue /// qvalue = ( "0" [ "." 0*3DIGIT ] ) /// / ( "1" [ "." 0*3("0") ] ) /// ``` /// /// # Example Values /// - `da, en-gb;q=0.8, en;q=0.7` /// - `en-us;q=1.0, en;q=0.5, fr` /// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptLanguage, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ /// "en-US".parse().unwrap(), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptLanguage, QualityItem, q}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ /// "da".parse().unwrap(), /// "en-GB;q=0.8".parse().unwrap(), /// "en;q=0.7".parse().unwrap(), /// ]) /// ); /// ``` (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* test_parse_and_format { common_header_test!(no_headers, [b""; 0], Some(AcceptLanguage(vec![]))); common_header_test!(empty_header, [b""; 1], Some(AcceptLanguage(vec![]))); common_header_test!( example_from_rfc, [b"da, en-gb;q=0.8, en;q=0.7"] ); common_header_test!( not_ordered_by_weight, [b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ QualityItem::max("en-US".parse().unwrap()), QualityItem::new("en".parse().unwrap(), q(0.5)), QualityItem::max("fr".parse().unwrap()), ])) ); common_header_test!( has_wildcard, [b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], Some(AcceptLanguage(vec![ QualityItem::max("fr-CH".parse().unwrap()), QualityItem::new("fr".parse().unwrap(), q(0.9)), QualityItem::new("en".parse().unwrap(), q(0.8)), QualityItem::new("de".parse().unwrap(), q(0.7)), QualityItem::new("*".parse().unwrap(), q(0.5)), ])) ); } } impl AcceptLanguage { /// Extracts the most preferable language, accounting for [q-factor weighting]. /// /// If no q-factors are provided, the first language is chosen. Note that items without /// q-factors are given the maximum preference value. /// /// As per the spec, returns [`Preference::Any`] if contained list is empty. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn preference(&self) -> Preference { let mut max_item = None; let mut max_pref = Quality::ZERO; // uses manual max lookup loop since we want the first occurrence in the case of same // preference but `Iterator::max_by_key` would give us the last occurrence for pref in &self.0 { // only change if strictly greater // equal items, even while unsorted, still have higher preference if they appear first if pref.quality > max_pref { max_pref = pref.quality; max_item = Some(pref.item.clone()); } } max_item.unwrap_or(Preference::Any) } /// Returns a sorted list of languages from highest to lowest precedence, accounting /// for [q-factor weighting]. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn ranked(&self) -> Vec> { if self.0.is_empty() { return vec![]; } let mut types = self.0.clone(); // use stable sort so items with equal q-factor retain listed order types.sort_by(|a, b| { // sort by q-factor descending b.quality.cmp(&a.quality) }); types.into_iter().map(|q_item| q_item.item).collect() } } #[cfg(test)] mod tests { use super::*; use crate::http::header::*; #[test] fn ranking_precedence() { let test = AcceptLanguage(vec![]); assert!(test.ranked().is_empty()); let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]); assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]); let test = AcceptLanguage(vec![ QualityItem::new("fr".parse().unwrap(), q(0.900)), QualityItem::new("fr-CH".parse().unwrap(), q(1.0)), QualityItem::new("en".parse().unwrap(), q(0.800)), QualityItem::new("*".parse().unwrap(), q(0.500)), QualityItem::new("de".parse().unwrap(), q(0.700)), ]); assert_eq!( test.ranked(), vec![ "fr-CH".parse().unwrap(), "fr".parse().unwrap(), "en".parse().unwrap(), "de".parse().unwrap(), "*".parse().unwrap(), ] ); let test = AcceptLanguage(vec![ QualityItem::max("fr".parse().unwrap()), QualityItem::max("fr-CH".parse().unwrap()), QualityItem::max("en".parse().unwrap()), QualityItem::max("*".parse().unwrap()), QualityItem::max("de".parse().unwrap()), ]); assert_eq!( test.ranked(), vec![ "fr".parse().unwrap(), "fr-CH".parse().unwrap(), "en".parse().unwrap(), "*".parse().unwrap(), "de".parse().unwrap(), ] ); } #[test] fn preference_selection() { let test = AcceptLanguage(vec![ QualityItem::new("fr".parse().unwrap(), q(0.900)), QualityItem::new("fr-CH".parse().unwrap(), q(1.0)), QualityItem::new("en".parse().unwrap(), q(0.800)), QualityItem::new("*".parse().unwrap(), q(0.500)), QualityItem::new("de".parse().unwrap(), q(0.700)), ]); assert_eq!( test.preference(), Preference::Specific("fr-CH".parse().unwrap()) ); let test = AcceptLanguage(vec![ QualityItem::max("fr".parse().unwrap()), QualityItem::max("fr-CH".parse().unwrap()), QualityItem::max("en".parse().unwrap()), QualityItem::max("*".parse().unwrap()), QualityItem::max("de".parse().unwrap()), ]); assert_eq!( test.preference(), Preference::Specific("fr".parse().unwrap()) ); let test = AcceptLanguage(vec![]); assert_eq!(test.preference(), Preference::Any); } } actix-web-4.9.0/src/http/header/allow.rs000064400000000000000000000042731046102023000162110ustar 00000000000000use actix_http::Method; use crate::http::header; crate::http::header::common_header! { /// `Allow` header, defined /// in [RFC 7231 §7.4.1](https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1) /// /// The `Allow` header field lists the set of methods advertised as /// supported by the target resource. The purpose of this field is /// strictly to inform the recipient of valid request methods associated /// with the resource. /// /// # ABNF /// ```plain /// Allow = #method /// ``` /// /// # Example Values /// * `GET, HEAD, PUT` /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` /// * `` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::{header::Allow, Method}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Allow(vec![Method::GET]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::{header::Allow, Method}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Allow(vec![ /// Method::GET, /// Method::POST, /// Method::PATCH, /// ]) /// ); /// ``` (Allow, header::ALLOW) => (Method)* test_parse_and_format { // from the RFC crate::http::header::common_header_test!( test1, [b"GET, HEAD, PUT"], Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); // other tests crate::http::header::common_header_test!( test2, [b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], Some(HeaderField(vec![ Method::OPTIONS, Method::GET, Method::PUT, Method::POST, Method::DELETE, Method::HEAD, Method::TRACE, Method::CONNECT, Method::PATCH]))); crate::http::header::common_header_test!( test3, [b""], Some(HeaderField(Vec::::new()))); } } actix-web-4.9.0/src/http/header/any_or_some.rs000064400000000000000000000037601046102023000174050ustar 00000000000000use std::{ fmt::{self, Write as _}, str, }; /// A wrapper for types used in header values where wildcard (`*`) items are allowed but the /// underlying type does not support them. /// /// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) /// typed header but it does parse `*` successfully. On the other hand, the `mime` crate, used for /// [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not /// used in those header types. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] pub enum AnyOrSome { /// A wildcard value. Any, /// A valid `T`. Item(T), } impl AnyOrSome { /// Returns true if item is wildcard (`*`) variant. pub fn is_any(&self) -> bool { matches!(self, Self::Any) } /// Returns true if item is a valid item (`T`) variant. pub fn is_item(&self) -> bool { matches!(self, Self::Item(_)) } /// Returns reference to value in `Item` variant, if it is set. pub fn item(&self) -> Option<&T> { match self { AnyOrSome::Item(ref item) => Some(item), AnyOrSome::Any => None, } } /// Consumes the container, returning the value in the `Item` variant, if it is set. pub fn into_item(self) -> Option { match self { AnyOrSome::Item(item) => Some(item), AnyOrSome::Any => None, } } } impl fmt::Display for AnyOrSome { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AnyOrSome::Any => f.write_char('*'), AnyOrSome::Item(item) => fmt::Display::fmt(item, f), } } } impl str::FromStr for AnyOrSome { type Err = T::Err; #[inline] fn from_str(s: &str) -> Result { match s.trim() { "*" => Ok(Self::Any), other => other.parse().map(AnyOrSome::Item), } } } actix-web-4.9.0/src/http/header/cache_control.rs000064400000000000000000000136621046102023000177000ustar 00000000000000use std::{fmt, str}; use super::common_header; use crate::http::header; common_header! { /// `Cache-Control` header, defined /// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2). /// /// The `Cache-Control` header field is used to specify directives for /// caches along the request/response chain. Such cache directives are /// unidirectional in that the presence of a directive in a request does /// not imply that the same directive is to be given in the response. /// /// # ABNF /// ```text /// Cache-Control = 1#cache-directive /// cache-directive = token [ "=" ( token / quoted-string ) ] /// ``` /// /// # Example Values /// * `no-cache` /// * `private, community="UCI"` /// * `max-age=30` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{CacheControl, CacheDirective}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{CacheControl, CacheDirective}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(CacheControl(vec![ /// CacheDirective::NoCache, /// CacheDirective::Private, /// CacheDirective::MaxAge(360u32), /// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), /// ])); /// ``` (CacheControl, header::CACHE_CONTROL) => (CacheDirective)+ test_parse_and_format { common_header_test!(no_headers, [b""; 0], None); common_header_test!(empty_header, [b""; 1], None); common_header_test!(bad_syntax, [b"foo="], None); common_header_test!( multiple_headers, [&b"no-cache"[..], &b"private"[..]], Some(CacheControl(vec![ CacheDirective::NoCache, CacheDirective::Private, ])) ); common_header_test!( argument, [b"max-age=100, private"], Some(CacheControl(vec![ CacheDirective::MaxAge(100), CacheDirective::Private, ])) ); common_header_test!( extension, [b"foo, bar=baz"], Some(CacheControl(vec![ CacheDirective::Extension("foo".to_owned(), None), CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), ])) ); #[test] fn parse_quote_form() { let req = test::TestRequest::default() .insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) .finish(); assert_eq!( Header::parse(&req).ok(), Some(CacheControl(vec![CacheDirective::MaxAge(200)])) ) } } } /// `CacheControl` contains a list of these directives. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CacheDirective { /// "no-cache" NoCache, /// "no-store" NoStore, /// "no-transform" NoTransform, /// "only-if-cached" OnlyIfCached, // request directives /// "max-age=delta" MaxAge(u32), /// "max-stale=delta" MaxStale(u32), /// "min-fresh=delta" MinFresh(u32), // response directives /// "must-revalidate" MustRevalidate, /// "public" Public, /// "private" Private, /// "proxy-revalidate" ProxyRevalidate, /// "s-maxage=delta" SMaxAge(u32), /// Extension directives. Optionally include an argument. Extension(String, Option), } impl fmt::Display for CacheDirective { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use self::CacheDirective::*; let dir_str = match self { NoCache => "no-cache", NoStore => "no-store", NoTransform => "no-transform", OnlyIfCached => "only-if-cached", MaxAge(secs) => return write!(f, "max-age={}", secs), MaxStale(secs) => return write!(f, "max-stale={}", secs), MinFresh(secs) => return write!(f, "min-fresh={}", secs), MustRevalidate => "must-revalidate", Public => "public", Private => "private", ProxyRevalidate => "proxy-revalidate", SMaxAge(secs) => return write!(f, "s-maxage={}", secs), Extension(name, None) => name.as_str(), Extension(name, Some(arg)) => return write!(f, "{}={}", name, arg), }; f.write_str(dir_str) } } impl str::FromStr for CacheDirective { type Err = Option<::Err>; fn from_str(s: &str) -> Result { use self::CacheDirective::*; match s { "" => Err(None), "no-cache" => Ok(NoCache), "no-store" => Ok(NoStore), "no-transform" => Ok(NoTransform), "only-if-cached" => Ok(OnlyIfCached), "must-revalidate" => Ok(MustRevalidate), "public" => Ok(Public), "private" => Ok(Private), "proxy-revalidate" => Ok(ProxyRevalidate), _ => match s.find('=') { Some(idx) if idx + 1 < s.len() => { match (&s[..idx], s[idx + 1..].trim_matches('"')) { ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), (left, right) => Ok(Extension(left.to_owned(), Some(right.to_owned()))), } } Some(_) => Err(None), None => Ok(Extension(s.to_owned(), None)), }, } } } actix-web-4.9.0/src/http/header/content_disposition.rs000064400000000000000000001161341046102023000211710ustar 00000000000000//! The `Content-Disposition` header and associated types. //! //! # References //! - "The Content-Disposition Header Field": //! //! - "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)": //! //! - "Returning Values from Forms: multipart/form-data": //! //! - Browser conformance tests at: //! - IANA assignment: use std::fmt::{self, Write}; use once_cell::sync::Lazy; #[cfg(feature = "unicode")] use regex::Regex; #[cfg(not(feature = "unicode"))] use regex_lite::Regex; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use crate::http::header; /// Split at the index of the first `needle` if it exists or at the end. fn split_once(haystack: &str, needle: char) -> (&str, &str) { haystack.find(needle).map_or_else( || (haystack, ""), |sc| { let (first, last) = haystack.split_at(sc); (first, last.split_at(1).1) }, ) } /// Split at the index of the first `needle` if it exists or at the end, trim the right of the /// first part and the left of the last part. fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { let (first, last) = split_once(haystack, needle); (first.trim_end(), last.trim_start()) } /// The implied disposition of the content of the HTTP body. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DispositionType { /// Inline implies default processing. Inline, /// Attachment implies that the recipient should prompt the user to save the response locally, /// rather than process it normally (as per its media type). Attachment, /// Used in *multipart/form-data* as defined in /// [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) to carry the field name and /// optional filename. FormData, /// Extension type. Should be handled by recipients the same way as Attachment. Ext(String), } impl<'a> From<&'a str> for DispositionType { fn from(origin: &'a str) -> DispositionType { if origin.eq_ignore_ascii_case("inline") { DispositionType::Inline } else if origin.eq_ignore_ascii_case("attachment") { DispositionType::Attachment } else if origin.eq_ignore_ascii_case("form-data") { DispositionType::FormData } else { DispositionType::Ext(origin.to_owned()) } } } /// Parameter in [`ContentDisposition`]. /// /// # Examples /// ``` /// use actix_web::http::header::DispositionParam; /// /// let param = DispositionParam::Filename(String::from("sample.txt")); /// assert!(param.is_filename()); /// assert_eq!(param.as_filename().unwrap(), "sample.txt"); /// ``` #[derive(Debug, Clone, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum DispositionParam { /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// the form. Name(String), /// A plain file name. /// /// It is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to contain /// any non-ASCII characters when used in a *Content-Disposition* HTTP response header, where /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// in case there are Unicode characters in file names. Filename(String), /// An extended file name. It must not exist for `ContentType::Formdata` according to /// [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2). FilenameExt(ExtendedValue), /// An unrecognized regular parameter as defined in /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as /// `reg-parameter`, in /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as /// `token "=" value`. Recipients should ignore unrecognizable parameters. Unknown(String, String), /// An unrecognized extended parameter as defined in /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as /// `ext-parameter`, in /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as /// `ext-token "=" ext-value`. The single trailing asterisk is not included. Recipients should /// ignore unrecognizable parameters. UnknownExt(String, ExtendedValue), } impl DispositionParam { /// Returns `true` if the parameter is [`Name`](DispositionParam::Name). #[inline] pub fn is_name(&self) -> bool { self.as_name().is_some() } /// Returns `true` if the parameter is [`Filename`](DispositionParam::Filename). #[inline] pub fn is_filename(&self) -> bool { self.as_filename().is_some() } /// Returns `true` if the parameter is [`FilenameExt`](DispositionParam::FilenameExt). #[inline] pub fn is_filename_ext(&self) -> bool { self.as_filename_ext().is_some() } /// Returns `true` if the parameter is [`Unknown`](DispositionParam::Unknown) and the `name` #[inline] /// matches. pub fn is_unknown>(&self, name: T) -> bool { self.as_unknown(name).is_some() } /// Returns `true` if the parameter is [`UnknownExt`](DispositionParam::UnknownExt) and the /// `name` matches. #[inline] pub fn is_unknown_ext>(&self, name: T) -> bool { self.as_unknown_ext(name).is_some() } /// Returns the name if applicable. #[inline] pub fn as_name(&self) -> Option<&str> { match self { DispositionParam::Name(name) => Some(name.as_str()), _ => None, } } /// Returns the filename if applicable. #[inline] pub fn as_filename(&self) -> Option<&str> { match self { DispositionParam::Filename(filename) => Some(filename.as_str()), _ => None, } } /// Returns the filename* if applicable. #[inline] pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { match self { DispositionParam::FilenameExt(value) => Some(value), _ => None, } } /// Returns the value of the unrecognized regular parameter if it is /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. #[inline] pub fn as_unknown>(&self, name: T) -> Option<&str> { match self { DispositionParam::Unknown(ref ext_name, ref value) if ext_name.eq_ignore_ascii_case(name.as_ref()) => { Some(value.as_str()) } _ => None, } } /// Returns the value of the unrecognized extended parameter if it is /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. #[inline] pub fn as_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { match self { DispositionParam::UnknownExt(ref ext_name, ref value) if ext_name.eq_ignore_ascii_case(name.as_ref()) => { Some(value) } _ => None, } } } /// A *Content-Disposition* header. It is compatible to be used either as /// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) /// as (re)defined in [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266), or as /// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) /// as (re)defined in [RFC 7587](https://datatracker.ietf.org/doc/html/rfc7578). /// /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as /// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be /// used to attach additional metadata, such as the filename to use when saving the response payload /// locally. /// /// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that /// can be used on the subpart of a multipart body to give information about the field it applies to. /// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body /// itself, *Content-Disposition* has no effect. /// /// # ABNF /// ```plain /// content-disposition = "Content-Disposition" ":" /// disposition-type *( ";" disposition-parm ) /// /// disposition-type = "inline" | "attachment" | disp-ext-type /// ; case-insensitive /// /// disp-ext-type = token /// /// disposition-parm = filename-parm | disp-ext-parm /// /// filename-parm = "filename" "=" value /// | "filename*" "=" ext-value /// /// disp-ext-parm = token "=" value /// | ext-token "=" ext-value /// /// ext-token = /// ``` /// /// # Note /// *filename* is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to /// contain any non-ASCII characters when used in a *Content-Disposition* HTTP response header, /// where filename* with charset UTF-8 may be used instead in case there are Unicode characters in /// file names. Filename is [acceptable](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) /// to be UTF-8 encoded directly in a *Content-Disposition* header for /// *multipart/form-data*, though. /// /// *filename* [must not](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) be used within /// *multipart/form-data*. /// /// # Examples /// ``` /// use actix_web::http::header::{ /// Charset, ContentDisposition, DispositionParam, DispositionType, /// ExtendedValue, /// }; /// /// let cd1 = ContentDisposition { /// disposition: DispositionType::Attachment, /// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { /// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename /// language_tag: None, // The optional language tag (see `language-tag` crate) /// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename /// })], /// }; /// assert!(cd1.is_attachment()); /// assert!(cd1.get_filename_ext().is_some()); /// /// let cd2 = ContentDisposition { /// disposition: DispositionType::FormData, /// parameters: vec![ /// DispositionParam::Name(String::from("file")), /// DispositionParam::Filename(String::from("bill.odt")), /// ], /// }; /// assert_eq!(cd2.get_name(), Some("file")); // field name /// assert_eq!(cd2.get_filename(), Some("bill.odt")); /// /// // HTTP response header with Unicode characters in file names /// let cd3 = ContentDisposition { /// disposition: DispositionType::Attachment, /// parameters: vec![ /// DispositionParam::FilenameExt(ExtendedValue { /// charset: Charset::Ext(String::from("UTF-8")), /// language_tag: None, /// value: String::from("\u{1f600}.svg").into_bytes(), /// }), /// // fallback for better compatibility /// DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg")) /// ], /// }; /// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()), /// Some("\u{1f600}.svg".as_bytes())); /// ``` /// /// # Security Note /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). #[derive(Debug, Clone, PartialEq, Eq)] pub struct ContentDisposition { /// The disposition type pub disposition: DispositionType, /// Disposition parameters pub parameters: Vec, } impl ContentDisposition { /// Constructs a Content-Disposition header suitable for downloads. /// /// # Examples /// ``` /// use actix_web::http::header::{ContentDisposition, TryIntoHeaderValue as _}; /// /// let cd = ContentDisposition::attachment("files.zip"); /// /// let cd_val = cd.try_into_value().unwrap(); /// assert_eq!(cd_val, "attachment; filename=\"files.zip\""); /// ``` pub fn attachment(filename: impl Into) -> Self { Self { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(filename.into())], } } /// Parse a raw Content-Disposition header value. pub fn from_raw(hv: &header::HeaderValue) -> Result { // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible // ASCII characters. So `hv.as_bytes` is necessary here. let hv = String::from_utf8(hv.as_bytes().to_vec()) .map_err(|_| crate::error::ParseError::Header)?; let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';'); if disp_type.is_empty() { return Err(crate::error::ParseError::Header); } let mut cd = ContentDisposition { disposition: disp_type.into(), parameters: Vec::new(), }; while !left.is_empty() { let (param_name, new_left) = split_once_and_trim(left, '='); if param_name.is_empty() || param_name == "*" || new_left.is_empty() { return Err(crate::error::ParseError::Header); } left = new_left; if let Some(param_name) = param_name.strip_suffix('*') { // extended parameters let (ext_value, new_left) = split_once_and_trim(left, ';'); left = new_left; let ext_value = header::parse_extended_value(ext_value)?; let param = if param_name.eq_ignore_ascii_case("filename") { DispositionParam::FilenameExt(ext_value) } else { DispositionParam::UnknownExt(param_name.to_owned(), ext_value) }; cd.parameters.push(param); } else { // regular parameters let value = if left.starts_with('\"') { // quoted-string: defined in RFC 6266 -> RFC 2616 Section 3.6 let mut escaping = false; let mut quoted_string = vec![]; let mut end = None; // search for closing quote for (i, &c) in left.as_bytes().iter().skip(1).enumerate() { if escaping { escaping = false; quoted_string.push(c); } else if c == 0x5c { // backslash escaping = true; } else if c == 0x22 { // double quote end = Some(i + 1); // cuz skipped 1 for the leading quote break; } else { quoted_string.push(c); } } left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..]; left = split_once(left, ';').1.trim_start(); // In fact, it should not be Err if the above code is correct. String::from_utf8(quoted_string) .map_err(|_| crate::error::ParseError::Header)? } else { // token: won't contains semicolon according to RFC 2616 Section 2.2 let (token, new_left) = split_once_and_trim(left, ';'); left = new_left; if token.is_empty() { // quoted-string can be empty, but token cannot be empty return Err(crate::error::ParseError::Header); } token.to_owned() }; let param = if param_name.eq_ignore_ascii_case("name") { DispositionParam::Name(value) } else if param_name.eq_ignore_ascii_case("filename") { // See also comments in test_from_raw_unnecessary_percent_decode. DispositionParam::Filename(value) } else { DispositionParam::Unknown(param_name.to_owned(), value) }; cd.parameters.push(param); } } Ok(cd) } /// Returns `true` if type is [`Inline`](DispositionType::Inline). pub fn is_inline(&self) -> bool { matches!(self.disposition, DispositionType::Inline) } /// Returns `true` if type is [`Attachment`](DispositionType::Attachment). pub fn is_attachment(&self) -> bool { matches!(self.disposition, DispositionType::Attachment) } /// Returns `true` if type is [`FormData`](DispositionType::FormData). pub fn is_form_data(&self) -> bool { matches!(self.disposition, DispositionType::FormData) } /// Returns `true` if type is [`Ext`](DispositionType::Ext) and the `disp_type` matches. pub fn is_ext(&self, disp_type: impl AsRef) -> bool { matches!( self.disposition, DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref()) ) } /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { self.parameters.iter().find_map(DispositionParam::as_name) } /// Return the value of *filename* if exists. pub fn get_filename(&self) -> Option<&str> { self.parameters .iter() .find_map(DispositionParam::as_filename) } /// Return the value of *filename\** if exists. pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { self.parameters .iter() .find_map(DispositionParam::as_filename_ext) } /// Return the value of the parameter which the `name` matches. pub fn get_unknown(&self, name: impl AsRef) -> Option<&str> { let name = name.as_ref(); self.parameters.iter().find_map(|p| p.as_unknown(name)) } /// Return the value of the extended parameter which the `name` matches. pub fn get_unknown_ext(&self, name: impl AsRef) -> Option<&ExtendedValue> { let name = name.as_ref(); self.parameters.iter().find_map(|p| p.as_unknown_ext(name)) } } impl TryIntoHeaderValue for ContentDisposition { type Error = header::InvalidHeaderValue; fn try_into_value(self) -> Result { let mut writer = Writer::new(); let _ = write!(&mut writer, "{}", self); header::HeaderValue::from_maybe_shared(writer.take()) } } impl Header for ContentDisposition { fn name() -> header::HeaderName { header::CONTENT_DISPOSITION } fn parse(msg: &T) -> Result { if let Some(h) = msg.headers().get(Self::name()) { Self::from_raw(h) } else { Err(crate::error::ParseError::Header) } } } impl fmt::Display for DispositionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { DispositionType::Inline => write!(f, "inline"), DispositionType::Attachment => write!(f, "attachment"), DispositionType::FormData => write!(f, "form-data"), DispositionType::Ext(ref s) => write!(f, "{}", s), } } } impl fmt::Display for DispositionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and // backslash should be escaped in quoted-string (i.e. "foobar"). // // Ref: RFC 6266 §4.1 -> RFC 2616 §3.6 // // filename-parm = "filename" "=" value // value = token | quoted-string // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) // qdtext = > // quoted-pair = "\" CHAR // TEXT = // LWS = [CRLF] 1*( SP | HT ) // OCTET = // CHAR = // CTL = // // Ref: RFC 7578 S4.2 -> RFC 2183 S2 -> RFC 2045 S5.1 // parameter := attribute "=" value // attribute := token // ; Matching of attributes // ; is ALWAYS case-insensitive. // value := token / quoted-string // token := 1* // tspecials := "(" / ")" / "<" / ">" / "@" / // "," / ";" / ":" / "\" / <"> // "/" / "[" / "]" / "?" / "=" // ; Must be in quoted-string, // ; to use within parameter values // // // See also comments in test_from_raw_unnecessary_percent_decode. static RE: Lazy = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); match self { DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Filename(ref value) => { write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) } DispositionParam::Unknown(ref name, ref value) => write!( f, "{}=\"{}\"", name, &RE.replace_all(value, "\\$0").as_ref() ), DispositionParam::FilenameExt(ref ext_value) => { write!(f, "filename*={}", ext_value) } DispositionParam::UnknownExt(ref name, ref ext_value) => { write!(f, "{}*={}", name, ext_value) } } } } impl fmt::Display for ContentDisposition { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.disposition)?; self.parameters .iter() .try_for_each(|param| write!(f, "; {}", param)) } } #[cfg(test)] mod tests { use super::{ContentDisposition, DispositionParam, DispositionType}; use crate::http::header::{Charset, ExtendedValue, HeaderValue}; #[test] fn test_from_raw_basic() { assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); let a = HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), DispositionParam::Name("upload".to_owned()), DispositionParam::Filename("sample.png".to_owned()), ], }; assert_eq!(a, b); let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], }; assert_eq!(a, b); let a = HeaderValue::from_static("inline; filename=image.jpg"); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], }; assert_eq!(a, b); let a = HeaderValue::from_static( "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Unknown( String::from("creation-date"), "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(), )], }; assert_eq!(a, b); } #[test] fn test_from_raw_extended() { let a = HeaderValue::from_static( "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's', ], })], }; assert_eq!(a, b); let a = HeaderValue::from_static( "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's', ], })], }; assert_eq!(a, b); } #[test] fn test_from_raw_extra_whitespace() { let a = HeaderValue::from_static( "form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()), DispositionParam::Name("upload".to_owned()), DispositionParam::Filename("sample.png".to_owned()), ], }; assert_eq!(a, b); } #[test] fn test_from_raw_unordered() { let a = HeaderValue::from_static( "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", // Actually, a trailing semicolon is not compliant. But it is fine to accept. ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), DispositionParam::Filename("sample.png".to_owned()), DispositionParam::Name("upload".to_owned()), ], }; assert_eq!(a, b); let a = HeaderValue::from_str( "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"", ) .unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Iso_8859_1, language_tag: None, value: b"foo-\xe4.html".to_vec(), }), DispositionParam::Filename("foo-ä.html".to_owned()), ], }; assert_eq!(a, b); } #[test] fn test_from_raw_only_disp() { let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![], }; assert_eq!(a, b); let a = ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![], }; assert_eq!(a, b); let a = ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext(String::from("unknown-disp-param")), parameters: vec![], }; assert_eq!(a, b); } #[test] fn from_raw_with_mixed_case() { let a = HeaderValue::from_str( "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"", ) .unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![ DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Iso_8859_1, language_tag: None, value: b"foo-\xe4.html".to_vec(), }), DispositionParam::Filename("foo-ä.html".to_owned()), ], }; assert_eq!(a, b); } #[test] fn from_raw_with_unicode() { /* RFC 7578 Section 4.2: Some commonly deployed systems use multipart/form-data with file names directly encoded including octets outside the US-ASCII range. The encoding used for the file names is typically UTF-8, although HTML forms will use the charset associated with the form. Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. (And now, only UTF-8 is handled by this implementation.) */ let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Name(String::from("upload")), DispositionParam::Filename(String::from("文件.webp")), ], }; assert_eq!(a, b); let a = HeaderValue::from_str( "form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"", ) .unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Name(String::from("upload")), DispositionParam::Filename(String::from("余固知謇謇之為患兮,忍而不能舍也.pptx")), ], }; assert_eq!(a, b); } #[test] fn test_from_raw_escape() { let a = HeaderValue::from_static( "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), DispositionParam::Name("upload".to_owned()), DispositionParam::Filename( ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g'] .iter() .collect(), ), ], }; assert_eq!(a, b); } #[test] fn test_from_raw_semicolon() { let a = HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![DispositionParam::Filename(String::from( "A semicolon here;.pdf", ))], }; assert_eq!(a, b); } #[test] fn test_from_raw_unnecessary_percent_decode() { // In fact, RFC 7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with // non-ASCII characters MAY be percent-encoded. // On the contrary, RFC 6266 or other RFCs related to Content-Disposition response header // do not mention such percent-encoding. // So, it appears to be undecidable whether to percent-decode or not without // knowing the usage scenario (multipart/form-data v.s. HTTP response header) and // inevitable to unnecessarily percent-decode filename with %XX in the former scenario. // Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file // names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without // percent-encoding. So we do not bother to attempt to percent-decode. let a = HeaderValue::from_static( "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Name("photo".to_owned()), DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")), ], }; assert_eq!(a, b); let a = HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Name("photo".to_owned()), DispositionParam::Filename(String::from("%74%65%73%74.png")), ], }; assert_eq!(a, b); } #[test] fn test_from_raw_param_value_missing() { let a = HeaderValue::from_static("form-data; name=upload ; filename="); assert!(ContentDisposition::from_raw(&a).is_err()); let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf"); assert!(ContentDisposition::from_raw(&a).is_err()); let a = HeaderValue::from_static("inline; filename= "); assert!(ContentDisposition::from_raw(&a).is_err()); let a = HeaderValue::from_static("inline; filename=\"\""); assert!(ContentDisposition::from_raw(&a) .expect("parse cd") .get_filename() .expect("filename") .is_empty()); } #[test] fn test_from_raw_param_name_missing() { let a = HeaderValue::from_static("inline; =\"test.txt\""); assert!(ContentDisposition::from_raw(&a).is_err()); let a = HeaderValue::from_static("inline; =diary.odt"); assert!(ContentDisposition::from_raw(&a).is_err()); let a = HeaderValue::from_static("inline; ="); assert!(ContentDisposition::from_raw(&a).is_err()); } #[test] fn test_display_extended() { let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; let a = HeaderValue::from_static(as_string); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); assert_eq!(as_string, display_rendered); let a = HeaderValue::from_static("attachment; filename=colourful.csv"); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); assert_eq!( "attachment; filename=\"colourful.csv\"".to_owned(), display_rendered ); } #[test] fn test_display_quote() { let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\""; as_string .find(['\\', '\"'].iter().collect::().as_str()) .unwrap(); // ensure `\"` is there let a = HeaderValue::from_static(as_string); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); assert_eq!(as_string, display_rendered); } #[test] fn test_display_space_tab() { let as_string = "form-data; name=upload; filename=\"Space here.png\""; let a = HeaderValue::from_static(as_string); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); assert_eq!(as_string, display_rendered); let a: ContentDisposition = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))], }; let display_rendered = format!("{}", a); assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered); } #[test] fn test_display_control_characters() { /* let a = "attachment; filename=\"carriage\rreturn.png\""; let a = HeaderValue::from_static(a); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); assert_eq!( "attachment; filename=\"carriage\\\rreturn.png\"", display_rendered );*/ // No way to create a HeaderValue containing a carriage return. let a: ContentDisposition = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))], }; let display_rendered = format!("{}", a); assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered); } #[test] fn test_param_methods() { let param = DispositionParam::Filename(String::from("sample.txt")); assert!(param.is_filename()); assert_eq!(param.as_filename().unwrap(), "sample.txt"); let param = DispositionParam::Unknown(String::from("foo"), String::from("bar")); assert!(param.is_unknown("foo")); assert_eq!(param.as_unknown("fOo"), Some("bar")); } #[test] fn test_disposition_methods() { let cd = ContentDisposition { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), DispositionParam::Name("upload".to_owned()), DispositionParam::Filename("sample.png".to_owned()), ], }; assert_eq!(cd.get_name(), Some("upload")); assert_eq!(cd.get_unknown("dummy"), Some("3")); assert_eq!(cd.get_filename(), Some("sample.png")); assert_eq!(cd.get_unknown_ext("dummy"), None); assert_eq!(cd.get_unknown("duMMy"), Some("3")); } } actix-web-4.9.0/src/http/header/content_language.rs000064400000000000000000000032761046102023000204120ustar 00000000000000use language_tags::LanguageTag; use super::{common_header, QualityItem, CONTENT_LANGUAGE}; common_header! { /// `Content-Language` header, defined /// in [RFC 7231 §3.1.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.3.2) /// /// The `Content-Language` header field describes the natural language(s) /// of the intended audience for the representation. Note that this /// might not be equivalent to all the languages used within the /// representation. /// /// # ABNF /// ```plain /// Content-Language = 1#language-tag /// ``` /// /// # Example Values /// * `da` /// * `mi, en` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentLanguage(vec![ /// QualityItem::max(LanguageTag::parse("en").unwrap()), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentLanguage(vec![ /// QualityItem::max(LanguageTag::parse("da").unwrap()), /// QualityItem::max(LanguageTag::parse("en-GB").unwrap()), /// ]) /// ); /// ``` (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ test_parse_and_format { crate::http::header::common_header_test!(test1, [b"da"]); crate::http::header::common_header_test!(test2, [b"mi, en"]); } } actix-web-4.9.0/src/http/header/content_length.rs000064400000000000000000000137701046102023000201100ustar 00000000000000use std::{convert::Infallible, str}; use derive_more::{Deref, DerefMut}; use crate::{ error::ParseError, http::header::{ from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue, CONTENT_LENGTH, }, HttpMessage, }; /// `Content-Length` header, defined in [RFC 9110 §8.6]. /// /// The Content-Length /// /// # ABNF /// /// ```plain /// Content-Length = 1*DIGIT /// ``` /// /// # Example Values /// /// - `0` /// - `3495` /// /// # Examples /// /// ``` /// use actix_web::{http::header::ContentLength, HttpResponse}; /// /// let res_empty = HttpResponse::Ok() /// .insert_header(ContentLength(0)); /// /// let res_fake_cl = HttpResponse::Ok() /// .insert_header(ContentLength(3_495)); /// ``` /// /// [RFC 9110 §8.6]: https://www.rfc-editor.org/rfc/rfc9110#name-content-length #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut)] pub struct ContentLength(pub usize); impl ContentLength { /// Returns Content-Length value. pub fn into_inner(&self) -> usize { self.0 } } impl str::FromStr for ContentLength { type Err = ::Err; #[inline] fn from_str(val: &str) -> Result { let val = val.trim(); // decoder prevents this case debug_assert!(!val.starts_with('+')); val.parse().map(Self) } } impl TryIntoHeaderValue for ContentLength { type Error = Infallible; fn try_into_value(self) -> Result { Ok(HeaderValue::from(self.0)) } } impl Header for ContentLength { fn name() -> HeaderName { CONTENT_LENGTH } fn parse(msg: &M) -> Result { let val = from_one_raw_str(msg.headers().get(Self::name()))?; // decoder prevents multiple CL headers debug_assert_eq!(msg.headers().get_all(Self::name()).count(), 1); Ok(val) } } impl From for usize { fn from(ContentLength(len): ContentLength) -> Self { len } } impl From for ContentLength { fn from(len: usize) -> Self { ContentLength(len) } } impl PartialEq for ContentLength { fn eq(&self, other: &usize) -> bool { self.0 == *other } } impl PartialEq for usize { fn eq(&self, other: &ContentLength) -> bool { *self == other.0 } } impl PartialOrd for ContentLength { fn partial_cmp(&self, other: &usize) -> Option { self.0.partial_cmp(other) } } impl PartialOrd for usize { fn partial_cmp(&self, other: &ContentLength) -> Option { self.partial_cmp(&other.0) } } #[cfg(test)] mod tests { use std::fmt; use super::*; use crate::{test::TestRequest, HttpRequest}; fn req_from_raw_headers, V: AsRef<[u8]>>( header_lines: I, ) -> HttpRequest { header_lines .into_iter() .fold(TestRequest::default(), |req, item| { req.append_header((H::name(), item.as_ref().to_vec())) }) .to_http_request() } #[track_caller] pub(crate) fn assert_parse_fail< H: Header + fmt::Debug, I: IntoIterator, V: AsRef<[u8]>, >( headers: I, ) { let req = req_from_raw_headers::(headers); H::parse(&req).unwrap_err(); } #[track_caller] pub(crate) fn assert_parse_eq< H: Header + fmt::Debug + PartialEq, I: IntoIterator, V: AsRef<[u8]>, >( headers: I, expect: H, ) { let req = req_from_raw_headers::(headers); assert_eq!(H::parse(&req).unwrap(), expect); } #[test] fn missing_header() { assert_parse_fail::([""; 0]); assert_parse_fail::([""]); } #[test] fn bad_header() { assert_parse_fail::(["-123"]); assert_parse_fail::(["123_456"]); assert_parse_fail::(["123.456"]); // too large for u64 (2^64, 2^64 + 1) assert_parse_fail::(["18446744073709551616"]); assert_parse_fail::(["18446744073709551617"]); // hex notation assert_parse_fail::(["0x123"]); // multi-value assert_parse_fail::(["0, 123"]); } #[test] #[should_panic] fn bad_header_plus() { // prevented by HTTP decoder anyway assert_parse_fail::(["+123"]); } #[test] #[should_panic] fn bad_multiple_value() { // prevented by HTTP decoder anyway assert_parse_fail::(["0", "123"]); } #[test] fn good_header() { assert_parse_eq::(["0"], ContentLength(0)); assert_parse_eq::(["1"], ContentLength(1)); assert_parse_eq::(["123"], ContentLength(123)); // value that looks like octal notation is not interpreted as such assert_parse_eq::(["0123"], ContentLength(123)); // whitespace variations assert_parse_eq::([" 0"], ContentLength(0)); assert_parse_eq::(["0 "], ContentLength(0)); assert_parse_eq::([" 0 "], ContentLength(0)); // large value (2^64 - 1) assert_parse_eq::( ["18446744073709551615"], ContentLength(18_446_744_073_709_551_615), ); } #[test] fn equality() { assert!(ContentLength(0) == ContentLength(0)); assert!(ContentLength(0) == 0); assert!(0 != ContentLength(123)); } #[test] fn ordering() { assert!(ContentLength(0) < ContentLength(123)); assert!(ContentLength(0) < 123); assert!(0 < ContentLength(123)); } } actix-web-4.9.0/src/http/header/content_range.rs000064400000000000000000000144061046102023000177200ustar 00000000000000use std::{ fmt::{self, Display, Write}, str::FromStr, }; use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer, CONTENT_RANGE}; use crate::error::ParseError; crate::http::header::common_header! { /// `Content-Range` header, defined /// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] test_parse_and_format { crate::http::header::common_header_test!(test_bytes, [b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: Some(500) }))); crate::http::header::common_header_test!(test_bytes_unknown_len, [b"bytes 0-499/*"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: None }))); crate::http::header::common_header_test!(test_bytes_unknown_range, [b"bytes */500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, instance_length: Some(500) }))); crate::http::header::common_header_test!(test_unregistered, [b"seconds 1-2"], Some(ContentRange(ContentRangeSpec::Unregistered { unit: "seconds".to_owned(), resp: "1-2".to_owned() }))); crate::http::header::common_header_test!(test_no_len, [b"bytes 0-499"], None::); crate::http::header::common_header_test!(test_only_unit, [b"bytes"], None::); crate::http::header::common_header_test!(test_end_less_than_start, [b"bytes 499-0/500"], None::); crate::http::header::common_header_test!(test_blank, [b""], None::); crate::http::header::common_header_test!(test_bytes_many_spaces, [b"bytes 1-2/500 3"], None::); crate::http::header::common_header_test!(test_bytes_many_slashes, [b"bytes 1-2/500/600"], None::); crate::http::header::common_header_test!(test_bytes_many_dashes, [b"bytes 1-2-3/500"], None::); } } /// Content-Range header, defined /// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) /// /// # ABNF /// ```plain /// Content-Range = byte-content-range /// / other-content-range /// /// byte-content-range = bytes-unit SP /// ( byte-range-resp / unsatisfied-range ) /// /// byte-range-resp = byte-range "/" ( complete-length / "*" ) /// byte-range = first-byte-pos "-" last-byte-pos /// unsatisfied-range = "*/" complete-length /// /// complete-length = 1*DIGIT /// /// other-content-range = other-range-unit SP other-range-resp /// other-range-resp = *CHAR /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum ContentRangeSpec { /// Byte range Bytes { /// First and last bytes of the range, omitted if request could not be /// satisfied range: Option<(u64, u64)>, /// Total length of the instance, can be omitted if unknown instance_length: Option, }, /// Custom range, with unit not registered at IANA Unregistered { /// other-range-unit unit: String, /// other-range-resp resp: String, }, } impl FromStr for ContentRangeSpec { type Err = ParseError; fn from_str(s: &str) -> Result { let res = match s.split_once(' ') { Some(("bytes", resp)) => { let (range, instance_length) = resp.split_once('/').ok_or(ParseError::Header)?; let instance_length = if instance_length == "*" { None } else { Some(instance_length.parse().map_err(|_| ParseError::Header)?) }; let range = if range == "*" { None } else { let (first_byte, last_byte) = range.split_once('-').ok_or(ParseError::Header)?; let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; if last_byte < first_byte { return Err(ParseError::Header); } Some((first_byte, last_byte)) }; ContentRangeSpec::Bytes { range, instance_length, } } Some((unit, resp)) => ContentRangeSpec::Unregistered { unit: unit.to_owned(), resp: resp.to_owned(), }, _ => return Err(ParseError::Header), }; Ok(res) } } impl Display for ContentRangeSpec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ContentRangeSpec::Bytes { range, instance_length, } => { f.write_str("bytes ")?; match range { Some((first_byte, last_byte)) => { write!(f, "{}-{}", first_byte, last_byte)?; } None => { f.write_str("*")?; } }; f.write_str("/")?; if let Some(v) = instance_length { write!(f, "{}", v) } else { f.write_str("*") } } ContentRangeSpec::Unregistered { ref unit, ref resp } => { f.write_str(unit)?; f.write_str(" ")?; f.write_str(resp) } } } } impl TryIntoHeaderValue for ContentRangeSpec { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { let mut writer = Writer::new(); let _ = write!(&mut writer, "{}", self); HeaderValue::from_maybe_shared(writer.take()) } } actix-web-4.9.0/src/http/header/content_type.rs000064400000000000000000000063531046102023000176070ustar 00000000000000use mime::Mime; use super::CONTENT_TYPE; crate::http::header::common_header! { /// `Content-Type` header, defined in [RFC 9110 §8.3]. /// /// The `Content-Type` header field indicates the media type of the associated representation: /// either the representation enclosed in the message payload or the selected representation, /// as determined by the message semantics. The indicated media type defines both the data /// format and how that data is intended to be processed by a recipient, within the scope of the /// received message semantics, after any content codings indicated by Content-Encoding are /// decoded. /// /// Although the `mime` crate allows the mime options to be any slice, this crate forces the use /// of Vec. This is to make sure the same header can't have more than 1 type. If this is an /// issue, it's possible to implement `Header` on a custom struct. /// /// # ABNF /// /// ```plain /// Content-Type = media-type /// ``` /// /// # Example Values /// /// - `text/html; charset=utf-8` /// - `application/json` /// /// # Examples /// /// ``` /// use actix_web::{http::header::ContentType, HttpResponse}; /// /// let res_json = HttpResponse::Ok() /// .insert_header(ContentType::json()); /// /// let res_html = HttpResponse::Ok() /// .insert_header(ContentType(mime::TEXT_HTML)); /// ``` /// /// [RFC 9110 §8.3]: https://datatracker.ietf.org/doc/html/rfc9110#section-8.3 (ContentType, CONTENT_TYPE) => [Mime] test_parse_and_format { crate::http::header::common_header_test!( test_text_html, [b"text/html"], Some(HeaderField(mime::TEXT_HTML))); crate::http::header::common_header_test!( test_image_star, [b"image/*"], Some(HeaderField(mime::IMAGE_STAR))); } } impl ContentType { /// Constructs a `Content-Type: application/json` header. #[inline] pub fn json() -> ContentType { ContentType(mime::APPLICATION_JSON) } /// Constructs a `Content-Type: text/plain; charset=utf-8` header. #[inline] pub fn plaintext() -> ContentType { ContentType(mime::TEXT_PLAIN_UTF_8) } /// Constructs a `Content-Type: text/html; charset=utf-8` header. #[inline] pub fn html() -> ContentType { ContentType(mime::TEXT_HTML_UTF_8) } /// Constructs a `Content-Type: text/xml` header. #[inline] pub fn xml() -> ContentType { ContentType(mime::TEXT_XML) } /// Constructs a `Content-Type: application/www-form-url-encoded` header. #[inline] pub fn form_url_encoded() -> ContentType { ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) } /// Constructs a `Content-Type: image/jpeg` header. #[inline] pub fn jpeg() -> ContentType { ContentType(mime::IMAGE_JPEG) } /// Constructs a `Content-Type: image/png` header. #[inline] pub fn png() -> ContentType { ContentType(mime::IMAGE_PNG) } /// Constructs a `Content-Type: application/octet-stream` header. #[inline] pub fn octet_stream() -> ContentType { ContentType(mime::APPLICATION_OCTET_STREAM) } } actix-web-4.9.0/src/http/header/date.rs000064400000000000000000000021271046102023000160040ustar 00000000000000use std::time::SystemTime; use super::{HttpDate, DATE}; crate::http::header::common_header! { /// `Date` header, defined /// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) /// /// The `Date` header field represents the date and time at which the /// message was originated. /// /// # ABNF /// ```plain /// Date = HTTP-date /// ``` /// /// # Example Values /// * `Tue, 15 Nov 1994 08:12:31 GMT` /// /// # Examples /// /// ``` /// use std::time::SystemTime; /// use actix_web::HttpResponse; /// use actix_web::http::header::Date; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Date(SystemTime::now().into()) /// ); /// ``` (Date, DATE) => [HttpDate] test_parse_and_format { crate::http::header::common_header_test!(test1, [b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } impl Date { /// Create a date instance set to the current system time pub fn now() -> Date { Date(SystemTime::now().into()) } } actix-web-4.9.0/src/http/header/encoding.rs000064400000000000000000000026341046102023000166600ustar 00000000000000use std::{fmt, str}; use actix_http::ContentEncoding; /// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Encoding { /// A supported content encoding. See [`ContentEncoding`] for variants. Known(ContentEncoding), /// Some other encoding that is less common, can be any string. Unknown(String), } impl Encoding { pub const fn identity() -> Self { Self::Known(ContentEncoding::Identity) } pub const fn brotli() -> Self { Self::Known(ContentEncoding::Brotli) } pub const fn deflate() -> Self { Self::Known(ContentEncoding::Deflate) } pub const fn gzip() -> Self { Self::Known(ContentEncoding::Gzip) } pub const fn zstd() -> Self { Self::Known(ContentEncoding::Zstd) } } impl fmt::Display for Encoding { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Encoding::Known(enc) => enc.as_str(), Encoding::Unknown(enc) => enc.as_str(), }) } } impl str::FromStr for Encoding { type Err = crate::error::ParseError; fn from_str(enc: &str) -> Result { match enc.parse::() { Ok(enc) => Ok(Self::Known(enc)), Err(_) => Ok(Self::Unknown(enc.to_owned())), } } } actix-web-4.9.0/src/http/header/entity.rs000064400000000000000000000222341046102023000164040ustar 00000000000000use std::{ fmt::{self, Display, Write}, str::FromStr, }; use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer}; /// check that each char in the slice is either: /// 1. `%x21`, or /// 2. in the range `%x23` to `%x7E`, or /// 3. above `%x80` fn entity_validate_char(c: u8) -> bool { c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80) } fn check_slice_validity(slice: &str) -> bool { slice.bytes().all(entity_validate_char) } /// An entity tag, defined in [RFC 7232 §2.3]. /// /// An entity tag consists of a string enclosed by two literal double quotes. /// Preceding the first double quote is an optional weakness indicator, /// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and /// `W/"xyzzy"`. /// /// # ABNF /// ```plain /// entity-tag = [ weak ] opaque-tag /// weak = %x57.2F ; "W/", case-sensitive /// opaque-tag = DQUOTE *etagc DQUOTE /// etagc = %x21 / %x23-7E / obs-text /// ; VCHAR except double quotes, plus obs-text /// ``` /// /// # Comparison /// To check if two entity tags are equivalent in an application always use the /// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use /// `==` to check if two tags are identical. /// /// The example below shows the results for a set of entity-tag pairs and /// both the weak and strong comparison function results: /// /// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | /// |---------|---------|-------------------|-----------------| /// | `W/"1"` | `W/"1"` | no match | match | /// | `W/"1"` | `W/"2"` | no match | no match | /// | `W/"1"` | `"1"` | no match | match | /// | `"1"` | `"1"` | match | match | /// /// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) #[derive(Debug, Clone, PartialEq, Eq)] pub struct EntityTag { /// Weakness indicator for the tag pub weak: bool, /// The opaque string in between the DQUOTEs tag: String, } impl EntityTag { /// Constructs a new `EntityTag`. /// /// # Panics /// If the tag contains invalid characters. pub fn new(weak: bool, tag: String) -> EntityTag { assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); EntityTag { weak, tag } } /// Constructs a new weak EntityTag. /// /// # Panics /// If the tag contains invalid characters. pub fn new_weak(tag: String) -> EntityTag { EntityTag::new(true, tag) } #[deprecated(since = "3.0.0", note = "Renamed to `new_weak`.")] pub fn weak(tag: String) -> EntityTag { Self::new_weak(tag) } /// Constructs a new strong EntityTag. /// /// # Panics /// If the tag contains invalid characters. pub fn new_strong(tag: String) -> EntityTag { EntityTag::new(false, tag) } #[deprecated(since = "3.0.0", note = "Renamed to `new_strong`.")] pub fn strong(tag: String) -> EntityTag { Self::new_strong(tag) } /// Returns tag. pub fn tag(&self) -> &str { self.tag.as_ref() } /// Sets tag. /// /// # Panics /// If the tag contains invalid characters. pub fn set_tag(&mut self, tag: impl Into) { let tag = tag.into(); assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); self.tag = tag } /// For strong comparison two entity-tags are equivalent if both are not weak and their /// opaque-tags match character-by-character. pub fn strong_eq(&self, other: &EntityTag) -> bool { !self.weak && !other.weak && self.tag == other.tag } /// For weak comparison two entity-tags are equivalent if their opaque-tags match /// character-by-character, regardless of either or both being tagged as "weak". pub fn weak_eq(&self, other: &EntityTag) -> bool { self.tag == other.tag } /// Returns the inverse of `strong_eq()`. pub fn strong_ne(&self, other: &EntityTag) -> bool { !self.strong_eq(other) } /// Returns inverse of `weak_eq()`. pub fn weak_ne(&self, other: &EntityTag) -> bool { !self.weak_eq(other) } } impl Display for EntityTag { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.weak { write!(f, "W/\"{}\"", self.tag) } else { write!(f, "\"{}\"", self.tag) } } } impl FromStr for EntityTag { type Err = crate::error::ParseError; fn from_str(slice: &str) -> Result { let length = slice.len(); // Early exits if it doesn't terminate in a DQUOTE. if !slice.ends_with('"') || slice.len() < 2 { return Err(crate::error::ParseError::Header); } // The etag is weak if its first char is not a DQUOTE. if slice.len() >= 2 && slice.starts_with('"') && check_slice_validity(&slice[1..length - 1]) { // No need to check if the last char is a DQUOTE, // we already did that above. return Ok(EntityTag { weak: false, tag: slice[1..length - 1].to_owned(), }); } else if slice.len() >= 4 && slice.starts_with("W/\"") && check_slice_validity(&slice[3..length - 1]) { return Ok(EntityTag { weak: true, tag: slice[3..length - 1].to_owned(), }); } Err(crate::error::ParseError::Header) } } impl TryIntoHeaderValue for EntityTag { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { let mut wrt = Writer::new(); write!(wrt, "{}", self).unwrap(); HeaderValue::from_maybe_shared(wrt.take()) } } #[cfg(test)] mod tests { use super::EntityTag; #[test] fn test_etag_parse_success() { // Expected success assert_eq!( "\"foobar\"".parse::().unwrap(), EntityTag::new_strong("foobar".to_owned()) ); assert_eq!( "\"\"".parse::().unwrap(), EntityTag::new_strong("".to_owned()) ); assert_eq!( "W/\"weaktag\"".parse::().unwrap(), EntityTag::new_weak("weaktag".to_owned()) ); assert_eq!( "W/\"\x65\x62\"".parse::().unwrap(), EntityTag::new_weak("\x65\x62".to_owned()) ); assert_eq!( "W/\"\"".parse::().unwrap(), EntityTag::new_weak("".to_owned()) ); } #[test] fn test_etag_parse_failures() { // Expected failures assert!("no-dquotes".parse::().is_err()); assert!("w/\"the-first-w-is-case-sensitive\"" .parse::() .is_err()); assert!("".parse::().is_err()); assert!("\"unmatched-dquotes1".parse::().is_err()); assert!("unmatched-dquotes2\"".parse::().is_err()); assert!("matched-\"dquotes\"".parse::().is_err()); } #[test] fn test_etag_fmt() { assert_eq!( format!("{}", EntityTag::new_strong("foobar".to_owned())), "\"foobar\"" ); assert_eq!(format!("{}", EntityTag::new_strong("".to_owned())), "\"\""); assert_eq!( format!("{}", EntityTag::new_weak("weak-etag".to_owned())), "W/\"weak-etag\"" ); assert_eq!( format!("{}", EntityTag::new_weak("\u{0065}".to_owned())), "W/\"\x65\"" ); assert_eq!(format!("{}", EntityTag::new_weak("".to_owned())), "W/\"\""); } #[test] fn test_cmp() { // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | // |---------|---------|-------------------|-----------------| // | `W/"1"` | `W/"1"` | no match | match | // | `W/"1"` | `W/"2"` | no match | no match | // | `W/"1"` | `"1"` | no match | match | // | `"1"` | `"1"` | match | match | let mut etag1 = EntityTag::new_weak("1".to_owned()); let mut etag2 = EntityTag::new_weak("1".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(!etag1.weak_ne(&etag2)); etag1 = EntityTag::new_weak("1".to_owned()); etag2 = EntityTag::new_weak("2".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(!etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(etag1.weak_ne(&etag2)); etag1 = EntityTag::new_weak("1".to_owned()); etag2 = EntityTag::new_strong("1".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(!etag1.weak_ne(&etag2)); etag1 = EntityTag::new_strong("1".to_owned()); etag2 = EntityTag::new_strong("1".to_owned()); assert!(etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(!etag1.strong_ne(&etag2)); assert!(!etag1.weak_ne(&etag2)); } } actix-web-4.9.0/src/http/header/etag.rs000064400000000000000000000072251046102023000160130ustar 00000000000000use super::{EntityTag, ETAG}; crate::http::header::common_header! { /// `ETag` header, defined in /// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) /// /// The `ETag` header field in a response provides the current entity-tag /// for the selected representation, as determined at the conclusion of /// handling the request. An entity-tag is an opaque validator for /// differentiating between multiple representations of the same /// resource, regardless of whether those multiple representations are /// due to resource state changes over time, content negotiation /// resulting in multiple representations being valid at the same time, /// or both. An entity-tag consists of an opaque quoted string, possibly /// prefixed by a weakness indicator. /// /// # ABNF /// ```plain /// ETag = entity-tag /// ``` /// /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `""` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ETag, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ETag(EntityTag::new_strong("xyzzy".to_owned())) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ETag, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ETag(EntityTag::new_weak("xyzzy".to_owned())) /// ); /// ``` (ETag, ETAG) => [EntityTag] test_parse_and_format { // From the RFC crate::http::header::common_header_test!(test1, [b"\"xyzzy\""], Some(ETag(EntityTag::new_strong("xyzzy".to_owned())))); crate::http::header::common_header_test!(test2, [b"W/\"xyzzy\""], Some(ETag(EntityTag::new_weak("xyzzy".to_owned())))); crate::http::header::common_header_test!(test3, [b"\"\""], Some(ETag(EntityTag::new_strong("".to_owned())))); // Own tests crate::http::header::common_header_test!(test4, [b"\"foobar\""], Some(ETag(EntityTag::new_strong("foobar".to_owned())))); crate::http::header::common_header_test!(test5, [b"\"\""], Some(ETag(EntityTag::new_strong("".to_owned())))); crate::http::header::common_header_test!(test6, [b"W/\"weak-etag\""], Some(ETag(EntityTag::new_weak("weak-etag".to_owned())))); crate::http::header::common_header_test!(test7, [b"W/\"\x65\x62\""], Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned())))); crate::http::header::common_header_test!(test8, [b"W/\"\""], Some(ETag(EntityTag::new_weak("".to_owned())))); crate::http::header::common_header_test!(test9, [b"no-dquotes"], None::); crate::http::header::common_header_test!(test10, [b"w/\"the-first-w-is-case-sensitive\""], None::); crate::http::header::common_header_test!(test11, [b""], None::); crate::http::header::common_header_test!(test12, [b"\"unmatched-dquotes1"], None::); crate::http::header::common_header_test!(test13, [b"unmatched-dquotes2\""], None::); crate::http::header::common_header_test!(test14, [b"matched-\"dquotes\""], None::); crate::http::header::common_header_test!(test15, [b"\""], None::); } } actix-web-4.9.0/src/http/header/expires.rs000064400000000000000000000023221046102023000165430ustar 00000000000000use super::{HttpDate, EXPIRES}; crate::http::header::common_header! { /// `Expires` header, defined /// in [RFC 7234 §5.3](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3) /// /// The `Expires` header field gives the date/time after which the /// response is considered stale. /// /// The presence of an Expires field does not imply that the original /// resource will change or cease to exist at, before, or after that /// time. /// /// # ABNF /// ```plain /// Expires = HTTP-date /// ``` /// /// # Example Values /// * `Thu, 01 Dec 1994 16:00:00 GMT` /// /// # Examples /// /// ``` /// use std::time::{SystemTime, Duration}; /// use actix_web::HttpResponse; /// use actix_web::http::header::Expires; /// /// let mut builder = HttpResponse::Ok(); /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// Expires(expiration.into()) /// ); /// ``` (Expires, EXPIRES) => [HttpDate] test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, [b"Thu, 01 Dec 1994 16:00:00 GMT"]); } } actix-web-4.9.0/src/http/header/if_match.rs000064400000000000000000000046741046102023000166520ustar 00000000000000use super::{common_header, EntityTag, IF_MATCH}; common_header! { /// `If-Match` header, defined /// in [RFC 7232 §3.1](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1) /// /// The `If-Match` header field makes the request method conditional on /// the recipient origin server either having at least one current /// representation of the target resource, when the field-value is "*", /// or having a current representation of the target resource that has an /// entity-tag matching a member of the list of entity-tags provided in /// the field-value. /// /// An origin server MUST use the strong comparison function when /// comparing entity-tags for `If-Match`, since the client /// intends this precondition to prevent the method from being applied if /// there have been any changes to the representation data. /// /// # ABNF /// ```plain /// If-Match = "*" / 1#entity-tag /// ``` /// /// # Example Values /// * `"xyzzy"` /// * "xyzzy", "r2d2xxxx", "c3piozzzz" /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::IfMatch; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(IfMatch::Any); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{IfMatch, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "bazquux".to_owned()), /// ]) /// ); /// ``` (IfMatch, IF_MATCH) => {Any / (EntityTag)+} test_parse_and_format { crate::http::header::common_header_test!( test1, [b"\"xyzzy\""], Some(HeaderField::Items( vec![EntityTag::new_strong("xyzzy".to_owned())]))); crate::http::header::common_header_test!( test2, [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( vec![EntityTag::new_strong("xyzzy".to_owned()), EntityTag::new_strong("r2d2xxxx".to_owned()), EntityTag::new_strong("c3piozzzz".to_owned())]))); crate::http::header::common_header_test!(test3, [b"*"], Some(IfMatch::Any)); } } actix-web-4.9.0/src/http/header/if_modified_since.rs000064400000000000000000000024761046102023000205150ustar 00000000000000use super::{HttpDate, IF_MODIFIED_SINCE}; crate::http::header::common_header! { /// `If-Modified-Since` header, defined /// in [RFC 7232 §3.3](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3) /// /// The `If-Modified-Since` header field makes a GET or HEAD request /// method conditional on the selected representation's modification date /// being more recent than the date provided in the field-value. /// Transfer of the selected representation's data is avoided if that /// data has not changed. /// /// # ABNF /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Examples /// /// ``` /// use std::time::{SystemTime, Duration}; /// use actix_web::HttpResponse; /// use actix_web::http::header::IfModifiedSince; /// /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfModifiedSince(modified.into()) /// ); /// ``` (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } actix-web-4.9.0/src/http/header/if_none_match.rs000064400000000000000000000063661046102023000176710ustar 00000000000000use super::{EntityTag, IF_NONE_MATCH}; crate::http::header::common_header! { /// `If-None-Match` header, defined /// in [RFC 7232 §3.2](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2) /// /// The `If-None-Match` header field makes the request method conditional /// on a recipient cache or origin server either not having any current /// representation of the target resource, when the field-value is "*", /// or having a selected representation with an entity-tag that does not /// match any of those listed in the field-value. /// /// A recipient MUST use the weak comparison function when comparing /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags /// can be used for cache validation even if there have been changes to /// the representation data. /// /// # ABNF /// ```plain /// If-None-Match = "*" / 1#entity-tag /// ``` /// /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` /// * `*` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::IfNoneMatch; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(IfNoneMatch::Any); /// ``` /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{IfNoneMatch, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfNoneMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "bazquux".to_owned()), /// ]) /// ); /// ``` (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} test_parse_and_format { crate::http::header::common_header_test!(test1, [b"\"xyzzy\""]); crate::http::header::common_header_test!(test2, [b"W/\"xyzzy\""]); crate::http::header::common_header_test!(test3, [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); crate::http::header::common_header_test!(test4, [b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); crate::http::header::common_header_test!(test5, [b"*"]); } } #[cfg(test)] mod tests { use actix_http::test::TestRequest; use super::IfNoneMatch; use crate::http::header::{EntityTag, Header, IF_NONE_MATCH}; #[test] fn test_if_none_match() { let req = TestRequest::default() .insert_header((IF_NONE_MATCH, "*")) .finish(); let mut if_none_match = IfNoneMatch::parse(&req); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); let req = TestRequest::default() .insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])) .finish(); if_none_match = Header::parse(&req); let mut entities: Vec = Vec::new(); let foobar_etag = EntityTag::new_strong("foobar".to_owned()); let weak_etag = EntityTag::new_weak("weak-etag".to_owned()); entities.push(foobar_etag); entities.push(weak_etag); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); } } actix-web-4.9.0/src/http/header/if_range.rs000064400000000000000000000070171046102023000166440ustar 00000000000000use std::fmt::{self, Display, Write}; use super::{ from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue, TryIntoHeaderValue, Writer, }; use crate::{error::ParseError, http::header, HttpMessage}; /// `If-Range` header, defined /// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) /// /// If a client has a partial copy of a representation and wishes to have /// an up-to-date copy of the entire representation, it could use the /// Range header field with a conditional GET (using either or both of /// If-Unmodified-Since and If-Match.) However, if the precondition /// fails because the representation has been modified, the client would /// then have to make a second request to obtain the entire current /// representation. /// /// The `If-Range` header field allows a client to \"short-circuit\" the /// second request. Informally, its meaning is as follows: if the /// representation is unchanged, send me the part(s) that I am requesting /// in Range; otherwise, send me the entire representation. /// /// # ABNF /// ```plain /// If-Range = entity-tag / HTTP-date /// ``` /// /// # Example Values /// /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// * `\"xyzzy\"` /// /// # Examples /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{EntityTag, IfRange}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfRange::EntityTag( /// EntityTag::new(false, "abc".to_owned()) /// ) /// ); /// ``` /// /// ``` /// use std::time::{Duration, SystemTime}; /// use actix_web::{http::header::IfRange, HttpResponse}; /// /// let mut builder = HttpResponse::Ok(); /// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfRange::Date(fetched.into()) /// ); /// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub enum IfRange { /// The entity-tag the client has of the resource. EntityTag(EntityTag), /// The date when the client retrieved the resource. Date(HttpDate), } impl Header for IfRange { fn name() -> HeaderName { header::IF_RANGE } #[inline] fn parse(msg: &T) -> Result where T: HttpMessage, { let etag: Result = from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(etag) = etag { return Ok(IfRange::EntityTag(etag)); } let date: Result = from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(date) = date { return Ok(IfRange::Date(date)); } Err(ParseError::Header) } } impl Display for IfRange { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { IfRange::EntityTag(ref x) => Display::fmt(x, f), IfRange::Date(ref x) => Display::fmt(x, f), } } } impl TryIntoHeaderValue for IfRange { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { let mut writer = Writer::new(); let _ = write!(&mut writer, "{}", self); HeaderValue::from_maybe_shared(writer.take()) } } #[cfg(test)] mod test_parse_and_format { use std::str; use super::IfRange as HeaderField; use crate::http::header::*; crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); crate::http::header::common_header_test!(test2, [b"\"abc\""]); crate::http::header::common_header_test!(test3, [b"this-is-invalid"], None::); } actix-web-4.9.0/src/http/header/if_unmodified_since.rs000064400000000000000000000025771046102023000210620ustar 00000000000000use super::{HttpDate, IF_UNMODIFIED_SINCE}; crate::http::header::common_header! { /// `If-Unmodified-Since` header, defined /// in [RFC 7232 §3.4](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4) /// /// The `If-Unmodified-Since` header field makes the request method /// conditional on the selected representation's last modification date /// being earlier than or equal to the date provided in the field-value. /// This field accomplishes the same purpose as If-Match for cases where /// the user agent does not have an entity-tag for the representation. /// /// # ABNF /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Examples /// /// ``` /// use std::time::{SystemTime, Duration}; /// use actix_web::HttpResponse; /// use actix_web::http::header::IfUnmodifiedSince; /// /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfUnmodifiedSince(modified.into()) /// ); /// ``` (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } actix-web-4.9.0/src/http/header/last_modified.rs000064400000000000000000000023461046102023000176750ustar 00000000000000use super::{HttpDate, LAST_MODIFIED}; crate::http::header::common_header! { /// `Last-Modified` header, defined /// in [RFC 7232 §2.2](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2) /// /// The `Last-Modified` header field in a response provides a timestamp /// indicating the date and time at which the origin server believes the /// selected representation was last modified, as determined at the /// conclusion of handling the request. /// /// # ABNF /// ```plain /// Expires = HTTP-date /// ``` /// /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Examples /// /// ``` /// use std::time::{SystemTime, Duration}; /// use actix_web::HttpResponse; /// use actix_web::http::header::LastModified; /// /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// LastModified(modified.into()) /// ); /// ``` (LastModified, LAST_MODIFIED) => [HttpDate] test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } actix-web-4.9.0/src/http/header/macros.rs000064400000000000000000000253541046102023000163620ustar 00000000000000macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { #[cfg(test)] mod $tm { #![allow(unused_imports)] use ::core::str; use ::actix_http::{Method, test}; use ::mime::*; use $crate::http::header::{self, *}; use super::{$id as HeaderField, *}; $($tf)* } } } #[cfg(test)] macro_rules! common_header_test { ($id:ident, $raw:expr) => { #[test] fn $id() { use ::actix_http::test; let raw = $raw; let headers = raw.iter().map(|x| x.to_vec()).collect::>(); let mut req = test::TestRequest::default(); for item in headers { req = req.append_header((HeaderField::name(), item)).take(); } let req = req.finish(); let value = HeaderField::parse(&req); let result = format!("{}", value.unwrap()); let expected = ::std::string::String::from_utf8(raw[0].to_vec()).unwrap(); let result_cmp: Vec = result .to_ascii_lowercase() .split(' ') .map(|x| x.to_owned()) .collect(); let expected_cmp: Vec = expected .to_ascii_lowercase() .split(' ') .map(|x| x.to_owned()) .collect(); assert_eq!(result_cmp.concat(), expected_cmp.concat()); } }; ($id:ident, $raw:expr, $exp:expr) => { #[test] fn $id() { use actix_http::test; let headers = $raw.iter().map(|x| x.to_vec()).collect::>(); let mut req = test::TestRequest::default(); for item in headers { req.append_header((HeaderField::name(), item)); } let req = req.finish(); let val = HeaderField::parse(&req); let exp: ::core::option::Option = $exp; // test parsing assert_eq!(val.ok(), exp); // test formatting if let Some(exp) = exp { let raw = &($raw)[..]; let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); let mut joined = String::new(); if let Some(s) = iter.next() { joined.push_str(s); for s in iter { joined.push_str(", "); joined.push_str(s); } } assert_eq!(format!("{}", exp), joined); } } }; } macro_rules! common_header { // TODO: these docs are wrong, there's no $n or $nn // $attrs:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header // $nn:expr: Nice name of the header // List header, zero or more items ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)*) => { $(#[$attrs])* #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] fn parse(msg: &M) -> Result { let headers = msg.headers().get_all(Self::name()); $crate::http::header::from_comma_delimited(headers).map($id) } } impl ::core::fmt::Display for $id { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; // List header, one or more items ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)+) => { $(#[$attrs])* #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] fn parse(msg: &M) -> Result{ let headers = msg.headers().get_all(Self::name()); $crate::http::header::from_comma_delimited(headers) .and_then(|items| { if items.is_empty() { Err($crate::error::ParseError::Header) } else { Ok($id(items)) } }) } } impl ::core::fmt::Display for $id { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; // Single value header ($(#[$attrs:meta])*($id:ident, $name:expr) => [$value:ty]) => { $(#[$attrs])* #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub $value); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] fn parse(msg: &M) -> Result { let header = msg.headers().get(Self::name()); $crate::http::header::from_one_raw_str(header).map($id) } } impl ::core::fmt::Display for $id { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { ::core::fmt::Display::fmt(&self.0, f) } } impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { self.0.try_into_value() } } }; // List header, one or more items with "*" option ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { $(#[$attrs])* #[derive(Clone, Debug, PartialEq, Eq)] pub enum $id { /// Any value is a match Any, /// Only the listed items are a match Items(Vec<$item>), } impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] fn parse(msg: &M) -> Result { let is_any = msg .headers() .get(Self::name()) .and_then(|hdr| hdr.to_str().ok()) .map(|hdr| hdr.trim() == "*"); if let Some(true) = is_any { Ok($id::Any) } else { let headers = msg.headers().get_all(Self::name()); Ok($id::Items($crate::http::header::from_comma_delimited(headers)?)) } } } impl ::core::fmt::Display for $id { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { match *self { $id::Any => f.write_str("*"), $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited(f, &fields[..]) } } } impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; // optional test module ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { $(#[$attrs])* ($id, $name) => ($item)* } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$attrs:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { $(#[$attrs])* ($id, $n) => ($item)+ } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$attrs:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { $(#[$attrs])* ($id, $name) => [$item] } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { $(#[$attrs])* ($id, $name) => {Any / ($item)+} } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; } pub(crate) use common_header; #[cfg(test)] pub(crate) use common_header_test; pub(crate) use common_header_test_module; actix-web-4.9.0/src/http/header/mod.rs000064400000000000000000000051011046102023000156410ustar 00000000000000//! A Collection of Header implementations for common HTTP Headers. //! //! ## Mime Types //! Several header fields use MIME values for their contents. Keeping with the strongly-typed theme, //! the [mime] crate is used in such headers as [`ContentType`] and [`Accept`]. use std::fmt; // re-export from actix-http // - header name / value types // - relevant traits for converting to header name / value // - all const header names // - header map // - the few typed headers from actix-http // - header parsing utils pub use actix_http::header::*; use bytes::{Bytes, BytesMut}; mod accept; mod accept_charset; mod accept_encoding; mod accept_language; mod allow; mod cache_control; mod content_disposition; mod content_language; mod content_length; mod content_range; mod content_type; mod date; mod encoding; mod entity; mod etag; mod expires; mod if_match; mod if_modified_since; mod if_none_match; mod if_range; mod if_unmodified_since; mod last_modified; mod macros; mod preference; mod range; #[cfg(test)] pub(crate) use self::macros::common_header_test; pub(crate) use self::macros::{common_header, common_header_test_module}; pub use self::{ accept::Accept, accept_charset::AcceptCharset, accept_encoding::AcceptEncoding, accept_language::AcceptLanguage, allow::Allow, cache_control::{CacheControl, CacheDirective}, content_disposition::{ContentDisposition, DispositionParam, DispositionType}, content_language::ContentLanguage, content_length::ContentLength, content_range::{ContentRange, ContentRangeSpec}, content_type::ContentType, date::Date, encoding::Encoding, entity::EntityTag, etag::ETag, expires::Expires, if_match::IfMatch, if_modified_since::IfModifiedSince, if_none_match::IfNoneMatch, if_range::IfRange, if_unmodified_since::IfUnmodifiedSince, last_modified::LastModified, preference::Preference, range::{ByteRangeSpec, Range}, }; /// Format writer ([`fmt::Write`]) for a [`BytesMut`]. #[derive(Debug, Default)] struct Writer { buf: BytesMut, } impl Writer { /// Constructs new bytes writer. pub fn new() -> Writer { Writer::default() } /// Splits bytes out of writer, leaving writer buffer empty. pub fn take(&mut self) -> Bytes { self.buf.split().freeze() } } impl fmt::Write for Writer { #[inline] fn write_str(&mut self, s: &str) -> fmt::Result { self.buf.extend_from_slice(s.as_bytes()); Ok(()) } #[inline] fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { fmt::write(self, args) } } actix-web-4.9.0/src/http/header/preference.rs000064400000000000000000000040721046102023000172060ustar 00000000000000use std::{ fmt::{self, Write as _}, str, }; /// A wrapper for types used in header values where wildcard (`*`) items are allowed but the /// underlying type does not support them. /// /// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) /// typed header but it does not parse `*` successfully. On the other hand, the `mime` crate, used /// for [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not /// used in those header types. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] pub enum Preference { /// A wildcard value. Any, /// A valid `T`. Specific(T), } impl Preference { /// Returns true if preference is the any/wildcard (`*`) value. pub fn is_any(&self) -> bool { matches!(self, Self::Any) } /// Returns true if preference is the specific item (`T`) variant. pub fn is_specific(&self) -> bool { matches!(self, Self::Specific(_)) } /// Returns reference to value in `Specific` variant, if it is set. pub fn item(&self) -> Option<&T> { match self { Preference::Specific(ref item) => Some(item), Preference::Any => None, } } /// Consumes the container, returning the value in the `Specific` variant, if it is set. pub fn into_item(self) -> Option { match self { Preference::Specific(item) => Some(item), Preference::Any => None, } } } impl fmt::Display for Preference { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Preference::Any => f.write_char('*'), Preference::Specific(item) => fmt::Display::fmt(item, f), } } } impl str::FromStr for Preference { type Err = T::Err; #[inline] fn from_str(s: &str) -> Result { match s.trim() { "*" => Ok(Self::Any), other => other.parse().map(Preference::Specific), } } } actix-web-4.9.0/src/http/header/range.rs000064400000000000000000000340521046102023000161650ustar 00000000000000use std::{ cmp, fmt::{self, Display, Write}, str::FromStr, }; use actix_http::{error::ParseError, header, HttpMessage}; use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer}; /// `Range` header, defined /// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) /// /// The "Range" header field on a GET request modifies the method semantics to request transfer of /// only one or more sub-ranges of the selected representation data, rather than the entire selected /// representation data. /// /// # ABNF /// ```plain /// Range = byte-ranges-specifier / other-ranges-specifier /// other-ranges-specifier = other-range-unit "=" other-range-set /// other-range-set = 1*VCHAR /// /// bytes-unit = "bytes" /// /// byte-ranges-specifier = bytes-unit "=" byte-range-set /// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) /// byte-range-spec = first-byte-pos "-" [last-byte-pos] /// suffix-byte-range-spec = "-" suffix-length /// suffix-length = 1*DIGIT /// first-byte-pos = 1*DIGIT /// last-byte-pos = 1*DIGIT /// ``` /// /// # Example Values /// * `bytes=1000-` /// * `bytes=-50` /// * `bytes=0-1,30-40` /// * `bytes=0-10,20-90,-100` /// * `custom_unit=0-123` /// * `custom_unit=xxx-yyy` /// /// # Examples /// ``` /// use actix_web::http::header::{Range, ByteRangeSpec}; /// use actix_web::HttpResponse; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(Range::Bytes( /// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::From(200)] /// )); /// builder.insert_header(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); /// builder.insert_header(Range::bytes(1, 100)); /// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Range { /// Byte range. Bytes(Vec), /// Custom range, with unit not registered at IANA. /// /// (`other-range-unit`: String , `other-range-set`: String) Unregistered(String, String), } /// A range of bytes to fetch. /// /// Each [`Range::Bytes`] header can contain one or more `ByteRangeSpec`s. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ByteRangeSpec { /// All bytes from `x` to `y`, inclusive. /// /// Serialized as `x-y`. /// /// Example: `bytes=500-999` would represent the second 500 bytes. FromTo(u64, u64), /// All bytes starting from `x`, inclusive. /// /// Serialized as `x-`. /// /// Example: For a file of 1000 bytes, `bytes=950-` would represent bytes 950-999, inclusive. From(u64), /// The last `y` bytes, inclusive. /// /// Using the spec terminology, this is `suffix-byte-range-spec`. Serialized as `-y`. /// /// Example: For a file of 1000 bytes, `bytes=-50` is equivalent to `bytes=950-`. Last(u64), } impl ByteRangeSpec { /// Given the full length of the entity, attempt to normalize the byte range into an satisfiable /// end-inclusive `(from, to)` range. /// /// The resulting range is guaranteed to be a satisfiable range within the bounds /// of `0 <= from <= to < full_length`. /// /// If the byte range is deemed unsatisfiable, `None` is returned. An unsatisfiable range is /// generally cause for a server to either reject the client request with a /// `416 Range Not Satisfiable` status code, or to simply ignore the range header and serve the /// full entity using a `200 OK` status code. /// /// This function closely follows [RFC 7233 §2.1]. As such, it considers ranges to be /// satisfiable if they meet the following conditions: /// /// > If a valid byte-range-set includes at least one byte-range-spec with a first-byte-pos that /// > is less than the current length of the representation, or at least one /// > suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set is /// > satisfiable. Otherwise, the byte-range-set is unsatisfiable. /// /// The function also computes remainder ranges based on the RFC: /// /// > If the last-byte-pos value is absent, or if the value is greater than or equal to the /// > current length of the representation data, the byte range is interpreted as the remainder /// > of the representation (i.e., the server replaces the value of last-byte-pos with a value /// > that is one less than the current length of the selected representation). /// /// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233 pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { // If the full length is zero, there is no satisfiable end-inclusive range. if full_length == 0 { return None; } match *self { ByteRangeSpec::FromTo(from, to) => { if from < full_length && from <= to { Some((from, cmp::min(to, full_length - 1))) } else { None } } ByteRangeSpec::From(from) => { if from < full_length { Some((from, full_length - 1)) } else { None } } ByteRangeSpec::Last(last) => { if last > 0 { // From the RFC: If the selected representation is shorter than the specified // suffix-length, the entire representation is used. if last > full_length { Some((0, full_length - 1)) } else { Some((full_length - last, full_length - 1)) } } else { None } } } } } impl Range { /// Constructs a common byte range header. /// /// Eg: `bytes=from-to` pub fn bytes(from: u64, to: u64) -> Range { Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) } /// Constructs a byte range header with multiple subranges. /// /// Eg: `bytes=from1-to1,from2-to2,fromX-toX` pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { Range::Bytes( ranges .into_iter() .map(|(from, to)| ByteRangeSpec::FromTo(from, to)) .collect(), ) } } impl fmt::Display for ByteRangeSpec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), ByteRangeSpec::From(pos) => write!(f, "{}-", pos), } } } impl fmt::Display for Range { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Range::Bytes(ranges) => { write!(f, "bytes=")?; for (i, range) in ranges.iter().enumerate() { if i != 0 { f.write_str(",")?; } Display::fmt(range, f)?; } Ok(()) } Range::Unregistered(unit, range_str) => { write!(f, "{}={}", unit, range_str) } } } } impl FromStr for Range { type Err = ParseError; fn from_str(s: &str) -> Result { let (unit, val) = s.split_once('=').ok_or(ParseError::Header)?; match (unit, val) { ("bytes", ranges) => { let ranges = from_comma_delimited(ranges); if ranges.is_empty() { return Err(ParseError::Header); } Ok(Range::Bytes(ranges)) } (_, "") => Err(ParseError::Header), ("", _) => Err(ParseError::Header), (unit, range_str) => Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())), } } } impl FromStr for ByteRangeSpec { type Err = ParseError; fn from_str(s: &str) -> Result { let (start, end) = s.split_once('-').ok_or(ParseError::Header)?; match (start, end) { ("", end) => end .parse() .or(Err(ParseError::Header)) .map(ByteRangeSpec::Last), (start, "") => start .parse() .or(Err(ParseError::Header)) .map(ByteRangeSpec::From), (start, end) => match (start.parse(), end.parse()) { (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)), _ => Err(ParseError::Header), }, } } } impl Header for Range { fn name() -> HeaderName { header::RANGE } #[inline] fn parse(msg: &T) -> Result { header::from_one_raw_str(msg.headers().get(Self::name())) } } impl TryIntoHeaderValue for Range { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { let mut wrt = Writer::new(); let _ = write!(wrt, "{}", self); HeaderValue::from_maybe_shared(wrt.take()) } } /// Parses 0 or more items out of a comma delimited string, ignoring invalid items. fn from_comma_delimited(s: &str) -> Vec { s.split(',') .filter_map(|x| match x.trim() { "" => None, y => Some(y), }) .filter_map(|x| x.parse().ok()) .collect() } #[cfg(test)] mod tests { use actix_http::{test::TestRequest, Request}; use super::*; fn req(s: &str) -> Request { TestRequest::default() .insert_header((header::RANGE, s)) .finish() } #[test] fn test_parse_bytes_range_valid() { let r: Range = Header::parse(&req("bytes=1-100")).unwrap(); let r2: Range = Header::parse(&req("bytes=1-100,-")).unwrap(); let r3 = Range::bytes(1, 100); assert_eq!(r, r2); assert_eq!(r2, r3); let r: Range = Header::parse(&req("bytes=1-100,200-")).unwrap(); let r2: Range = Header::parse(&req("bytes= 1-100 , 101-xxx, 200- ")).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::From(200), ]); assert_eq!(r, r2); assert_eq!(r2, r3); let r: Range = Header::parse(&req("bytes=1-100,-100")).unwrap(); let r2: Range = Header::parse(&req("bytes=1-100, ,,-100")).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::Last(100), ]); assert_eq!(r, r2); assert_eq!(r2, r3); let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); assert_eq!(r, r2); } #[test] fn test_parse_unregistered_range_valid() { let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); assert_eq!(r, r2); let r: Range = Header::parse(&req("custom=abcd")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); assert_eq!(r, r2); let r: Range = Header::parse(&req("custom=xxx-yyy")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); assert_eq!(r, r2); } #[test] fn test_parse_invalid() { let r: Result = Header::parse(&req("bytes=1-a,-")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("bytes=1-2-3")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("abc")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("bytes=1-100=")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("bytes=")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("custom=")); assert_eq!(r.ok(), None); let r: Result = Header::parse(&req("=1-100")); assert_eq!(r.ok(), None); } #[test] fn test_fmt() { let range = Range::Bytes(vec![ ByteRangeSpec::FromTo(0, 1000), ByteRangeSpec::From(2000), ]); assert_eq!(&range.to_string(), "bytes=0-1000,2000-"); let range = Range::Bytes(vec![]); assert_eq!(&range.to_string(), "bytes="); let range = Range::Unregistered("custom".to_owned(), "1-xxx".to_owned()); assert_eq!(&range.to_string(), "custom=1-xxx"); } #[test] fn test_byte_range_spec_to_satisfiable_range() { assert_eq!( Some((0, 0)), ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) ); assert_eq!( Some((1, 2)), ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) ); assert_eq!( Some((1, 2)), ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) ); assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0)); assert_eq!(Some((0, 2)), ByteRangeSpec::From(0).to_satisfiable_range(3)); assert_eq!(Some((2, 2)), ByteRangeSpec::From(2).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::From(3).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::From(5).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::From(0).to_satisfiable_range(0)); assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3)); assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3)); assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); } } actix-web-4.9.0/src/http/mod.rs000064400000000000000000000002211046102023000144070ustar 00000000000000//! Various HTTP related types. pub mod header; pub use actix_http::{uri, ConnectionType, Error, KeepAlive, Method, StatusCode, Uri, Version}; actix-web-4.9.0/src/info.rs000064400000000000000000000404161046102023000136160ustar 00000000000000use std::{convert::Infallible, net::SocketAddr}; use actix_utils::future::{err, ok, Ready}; use derive_more::{Display, Error}; use crate::{ dev::{AppConfig, Payload, RequestHead}, http::{ header::{self, HeaderName}, uri::{Authority, Scheme}, }, FromRequest, HttpRequest, ResponseError, }; static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host"); static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto"); /// Trim whitespace then any quote marks. fn unquote(val: &str) -> &str { val.trim().trim_start_matches('"').trim_end_matches('"') } /// Remove port and IPv6 square brackets from a peer specification. fn bare_address(val: &str) -> &str { if val.starts_with('[') { val.split("]:") .next() .map(|s| s.trim_start_matches('[').trim_end_matches(']')) // this indicates that the IPv6 address is malformed so shouldn't // usually happen, but if it does, just return the original input .unwrap_or(val) } else { val.split(':').next().unwrap_or(val) } } /// Extracts and trims first value for given header name. fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { let hdr = req.headers.get(name)?.to_str().ok()?; let val = hdr.split(',').next()?.trim(); Some(val) } /// HTTP connection information. /// /// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers. /// /// # Examples /// ``` /// # use actix_web::{HttpResponse, Responder}; /// use actix_web::dev::ConnectionInfo; /// /// async fn handler(conn: ConnectionInfo) -> impl Responder { /// match conn.host() { /// "actix.rs" => HttpResponse::Ok().body("Welcome!"), /// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."), /// _ => HttpResponse::NotFound().finish() /// } /// } /// # let _svc = actix_web::web::to(handler); /// ``` /// /// # Implementation Notes /// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to /// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return /// strings instead of IP addresses or other types to acknowledge that they may be /// [obfuscated][rfc7239-63] or [unknown][rfc7239-62]. /// /// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` /// is preferred. /// /// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 /// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 /// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { host: String, scheme: String, peer_addr: Option, realip_remote_addr: Option, } impl ConnectionInfo { pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut realip_remote_addr = None; for (name, val) in req .headers .get_all(&header::FORWARDED) .filter_map(|hdr| hdr.to_str().ok()) // "for=1.2.3.4, for=5.6.7.8; scheme=https" .flat_map(|val| val.split(';')) // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"] .flat_map(|vals| vals.split(',')) // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] .flat_map(|pair| { let mut items = pair.trim().splitn(2, '='); Some((items.next()?, items.next()?)) }) { // [(name , val ), ... ] // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")] // taking the first value for each property is correct because spec states that first // "for" value is client and rest are proxies; multiple values other properties have // no defined semantics // // > In a chain of proxy servers where this is fully utilized, the first // > "for" parameter will disclose the client where the request was first // > made, followed by any subsequent proxy identifiers. // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 match name.trim().to_lowercase().as_str() { "for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))), "proto" => scheme.get_or_insert_with(|| unquote(val)), "host" => host.get_or_insert_with(|| unquote(val)), "by" => { // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 continue; } _ => continue, }; } let scheme = scheme .or_else(|| first_header_value(req, &X_FORWARDED_PROTO)) .or_else(|| req.uri.scheme().map(Scheme::as_str)) .or_else(|| Some("https").filter(|_| cfg.secure())) .unwrap_or("http") .to_owned(); let host = host .or_else(|| first_header_value(req, &X_FORWARDED_HOST)) .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) .or_else(|| req.uri.authority().map(Authority::as_str)) .unwrap_or_else(|| cfg.host()) .to_owned(); let realip_remote_addr = realip_remote_addr .or_else(|| first_header_value(req, &X_FORWARDED_FOR)) .map(str::to_owned); let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); ConnectionInfo { host, scheme, peer_addr, realip_remote_addr, } } /// Real IP (remote address) of client that initiated request. /// /// The address is resolved through the following, in order: /// - `Forwarded` header /// - `X-Forwarded-For` header /// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr)) /// /// # Security /// Do not use this function for security purposes unless you can be sure that the `Forwarded` /// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a /// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate. #[inline] pub fn realip_remote_addr(&self) -> Option<&str> { self.realip_remote_addr .as_deref() .or(self.peer_addr.as_deref()) } /// Returns serialized IP address of the peer connection. /// /// See [`HttpRequest::peer_addr`] for more details. #[inline] pub fn peer_addr(&self) -> Option<&str> { self.peer_addr.as_deref() } /// Hostname of the request. /// /// Hostname is resolved through the following, in order: /// - `Forwarded` header /// - `X-Forwarded-Host` header /// - `Host` header /// - request target / URI /// - configured server hostname #[inline] pub fn host(&self) -> &str { &self.host } /// Scheme of the request. /// /// Scheme is resolved through the following, in order: /// - `Forwarded` header /// - `X-Forwarded-Proto` header /// - request target / URI #[inline] pub fn scheme(&self) -> &str { &self.scheme } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")] pub fn remote_addr(&self) -> Option<&str> { self.peer_addr() } } impl FromRequest for ConnectionInfo { type Error = Infallible; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.connection_info().clone()) } } /// Extractor for peer's socket address. /// /// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`]. /// /// # Examples /// ``` /// # use actix_web::Responder; /// use actix_web::dev::PeerAddr; /// /// async fn handler(peer_addr: PeerAddr) -> impl Responder { /// let socket_addr = peer_addr.0; /// socket_addr.to_string() /// } /// # let _svc = actix_web::web::to(handler); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] #[display(fmt = "{}", _0)] pub struct PeerAddr(pub SocketAddr); impl PeerAddr { /// Unwrap into inner `SocketAddr` value. pub fn into_inner(self) -> SocketAddr { self.0 } } #[derive(Debug, Display, Error)] #[non_exhaustive] #[display(fmt = "Missing peer address")] pub struct MissingPeerAddr; impl ResponseError for MissingPeerAddr {} impl FromRequest for PeerAddr { type Error = MissingPeerAddr; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match req.peer_addr() { Some(addr) => ok(PeerAddr(addr)), None => { log::error!("Missing peer address."); err(MissingPeerAddr) } } } } #[cfg(test)] mod tests { use super::*; use crate::test::TestRequest; const X_FORWARDED_FOR: &str = "x-forwarded-for"; const X_FORWARDED_HOST: &str = "x-forwarded-host"; const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; #[test] fn info_default() { let req = TestRequest::default().to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); } #[test] fn host_header() { let req = TestRequest::default() .insert_header((header::HOST, "rust-lang.org")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.realip_remote_addr(), None); } #[test] fn x_forwarded_for_header() { let req = TestRequest::default() .insert_header((X_FORWARDED_FOR, "192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn x_forwarded_host_header() { let req = TestRequest::default() .insert_header((X_FORWARDED_HOST, "192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.host(), "192.0.2.60"); assert_eq!(info.realip_remote_addr(), None); } #[test] fn x_forwarded_proto_header() { let req = TestRequest::default() .insert_header((X_FORWARDED_PROTO, "https")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } #[test] fn forwarded_header() { let req = TestRequest::default() .insert_header(( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", )) .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() .insert_header(( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", )) .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn forwarded_case_sensitivity() { let req = TestRequest::default() .insert_header((header::FORWARDED, "For=192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn forwarded_weird_whitespace() { let req = TestRequest::default() .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); assert_eq!(info.scheme(), "https"); let req = TestRequest::default() .insert_header((header::FORWARDED, " for = 1.2.3.4 ")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); } #[test] fn forwarded_for_quoted() { let req = TestRequest::default() .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn forwarded_for_ipv6() { let req = TestRequest::default() .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#)) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17")); } #[test] fn forwarded_for_ipv6_with_port() { let req = TestRequest::default() .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17")); } #[test] fn forwarded_for_multiple() { let req = TestRequest::default() .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) .to_http_request(); let info = req.connection_info(); // takes the first value assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn scheme_from_uri() { let req = TestRequest::get() .uri("https://actix.rs/test") .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } #[test] fn host_from_uri() { let req = TestRequest::get() .uri("https://actix.rs/test") .to_http_request(); let info = req.connection_info(); assert_eq!(info.host(), "actix.rs"); } #[test] fn host_from_server_hostname() { let mut req = TestRequest::get(); req.set_server_hostname("actix.rs"); let req = req.to_http_request(); let info = req.connection_info(); assert_eq!(info.host(), "actix.rs"); } #[actix_rt::test] async fn conn_info_extract() { let req = TestRequest::default() .uri("https://actix.rs/test") .to_http_request(); let conn_info = ConnectionInfo::extract(&req).await.unwrap(); assert_eq!(conn_info.scheme(), "https"); assert_eq!(conn_info.host(), "actix.rs"); } #[actix_rt::test] async fn peer_addr_extract() { let req = TestRequest::default().to_http_request(); let res = PeerAddr::extract(&req).await; assert!(res.is_err()); let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let peer_addr = PeerAddr::extract(&req).await.unwrap(); assert_eq!(peer_addr, PeerAddr(addr)); } #[actix_rt::test] async fn remote_address() { let req = TestRequest::default().to_http_request(); let res = ConnectionInfo::extract(&req).await.unwrap(); assert!(res.peer_addr().is_none()); let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let conn_info = ConnectionInfo::extract(&req).await.unwrap(); assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1"); } #[actix_rt::test] async fn real_ip_from_socket_addr() { let req = TestRequest::default().to_http_request(); let res = ConnectionInfo::extract(&req).await.unwrap(); assert!(res.realip_remote_addr().is_none()); let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let conn_info = ConnectionInfo::extract(&req).await.unwrap(); assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1"); } } actix-web-4.9.0/src/lib.rs000064400000000000000000000110771046102023000134320ustar 00000000000000//! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. //! //! # Examples //! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! //! #[get("/hello/{name}")] //! async fn greet(name: web::Path) -> impl Responder { //! format!("Hello {}!", name) //! } //! //! #[actix_web::main] // or #[tokio::main] //! async fn main() -> std::io::Result<()> { //! HttpServer::new(|| { //! App::new().service(greet) //! }) //! .bind(("127.0.0.1", 8080))? //! .run() //! .await //! } //! ``` //! //! # Documentation & Community Resources //! In addition to this API documentation, several other resources are available: //! //! * [Website & User Guide](https://actix.rs/) //! * [Examples Repository](https://github.com/actix/examples) //! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x) //! //! To get started navigating the API docs, you may consider looking at the following pages first: //! //! * [`App`]: This struct represents an Actix Web application and is used to //! configure routes and other common application settings. //! //! * [`HttpServer`]: This struct represents an HTTP server instance and is //! used to instantiate and configure servers. //! //! * [`web`]: This module provides essential types for route registration as well as //! common utilities for request handlers. //! //! * [`HttpRequest`] and [`HttpResponse`]: These //! structs represent HTTP requests and responses and expose methods for creating, inspecting, //! and otherwise utilizing them. //! //! # Features //! - Supports HTTP/1.x and HTTP/2 //! - Streaming and pipelining //! - Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros //! - Full [Tokio](https://tokio.rs) compatibility //! - Keep-alive and slow requests handling //! - Client/server [WebSockets](https://actix.rs/docs/websockets/) support //! - Transparent content compression/decompression (br, gzip, deflate, zstd) //! - Multipart streams //! - Static assets //! - SSL support using OpenSSL or Rustls //! - Middlewares ([Logger, Session, CORS, etc](middleware)) //! - Integrates with the [`awc` HTTP client](https://docs.rs/awc/) //! - Runs on stable Rust 1.54+ //! //! # Crate Features //! - `cookies` - cookies support (enabled by default) //! - `macros` - routing and runtime macros (enabled by default) //! - `compress-brotli` - brotli content encoding compression support (enabled by default) //! - `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) //! - `compress-zstd` - zstd content encoding compression support (enabled by default) //! - `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` //! - `rustls` - HTTPS support via `rustls` 0.20 crate, supports `HTTP/2` //! - `rustls-0_21` - HTTPS support via `rustls` 0.21 crate, supports `HTTP/2` //! - `rustls-0_22` - HTTPS support via `rustls` 0.22 crate, supports `HTTP/2` //! - `rustls-0_23` - HTTPS support via `rustls` 0.23 crate, supports `HTTP/2` //! - `secure-cookies` - secure cookies support #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use actix_http::{body, HttpMessage}; #[cfg(feature = "cookies")] #[doc(inline)] pub use cookie; mod app; mod app_service; mod config; mod data; pub mod dev; pub mod error; mod extract; pub mod guard; mod handler; mod helpers; pub mod http; mod info; pub mod middleware; mod redirect; mod request; mod request_data; mod resource; mod response; mod rmap; mod route; pub mod rt; mod scope; mod server; mod service; pub mod test; mod thin_data; pub(crate) mod types; pub mod web; #[doc(inline)] pub use crate::error::Result; pub use crate::{ app::App, error::{Error, ResponseError}, extract::FromRequest, handler::Handler, request::HttpRequest, resource::Resource, response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}, route::Route, scope::Scope, server::HttpServer, types::Either, }; macro_rules! codegen_reexport { ($name:ident) => { #[cfg(feature = "macros")] pub use actix_web_codegen::$name; }; } codegen_reexport!(main); codegen_reexport!(test); codegen_reexport!(route); codegen_reexport!(routes); codegen_reexport!(head); codegen_reexport!(get); codegen_reexport!(post); codegen_reexport!(patch); codegen_reexport!(put); codegen_reexport!(delete); codegen_reexport!(trace); codegen_reexport!(connect); codegen_reexport!(options); codegen_reexport!(scope); pub(crate) type BoxError = Box; actix-web-4.9.0/src/middleware/authors-guide.md000064400000000000000000000005001046102023000175220ustar 00000000000000# Middleware Author's Guide ## What Is A Middleware? ## Middleware Traits ## Understanding Body Types ## Best Practices ## Error Propagation ## When To (Not) Use Middleware ## Author's References - `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 actix-web-4.9.0/src/middleware/compat.rs000064400000000000000000000141201046102023000162540ustar 00000000000000//! For middleware documentation, see [`Compat`]. use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; use crate::{ body::{BoxBody, MessageBody}, dev::{Service, Transform}, error::Error, service::ServiceResponse, }; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), /// and [`Condition`](super::Condition). /// /// # Examples /// ``` /// use actix_web::middleware::{Logger, Compat}; /// use actix_web::{App, web}; /// /// let logger = Logger::default(); /// /// // this would not compile because of incompatible body types /// // let app = App::new() /// // .service(web::scope("scoped").wrap(logger)); /// /// // by using this middleware we can use the logger on a scope /// let app = App::new() /// .service(web::scope("scoped").wrap(Compat::new(logger))); /// ``` pub struct Compat { transform: T, } impl Compat { /// Wrap a middleware to give it broader compatibility. pub fn new(middleware: T) -> Self { Self { transform: middleware, } } } impl Transform for Compat where S: Service, T: Transform, T::Future: 'static, T::Response: MapServiceResponseBody, T::Error: Into, { type Response = ServiceResponse; type Error = Error; type Transform = CompatMiddleware; type InitError = T::InitError; type Future = LocalBoxFuture<'static, Result>; fn new_transform(&self, service: S) -> Self::Future { let fut = self.transform.new_transform(service); Box::pin(async move { let service = fut.await?; Ok(CompatMiddleware { service }) }) } } pub struct CompatMiddleware { service: S, } impl Service for CompatMiddleware where S: Service, S::Response: MapServiceResponseBody, S::Error: Into, { type Response = ServiceResponse; type Error = Error; type Future = CompatMiddlewareFuture; actix_service::forward_ready!(service); fn call(&self, req: Req) -> Self::Future { let fut = self.service.call(req); CompatMiddlewareFuture { fut } } } pin_project! { pub struct CompatMiddlewareFuture { #[pin] fut: Fut, } } impl Future for CompatMiddlewareFuture where Fut: Future>, T: MapServiceResponseBody, E: Into, { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let res = match ready!(self.project().fut.poll(cx)) { Ok(res) => res, Err(err) => return Poll::Ready(Err(err.into())), }; Poll::Ready(Ok(res.map_body())) } } /// Convert `ServiceResponse`'s `ResponseBody` generic type to `ResponseBody`. pub trait MapServiceResponseBody { fn map_body(self) -> ServiceResponse; } impl MapServiceResponseBody for ServiceResponse where B: MessageBody + 'static, { #[inline] fn map_body(self) -> ServiceResponse { self.map_into_boxed_body() } } #[cfg(test)] mod tests { // easier to code when cookies feature is disabled #![allow(unused_imports)] use actix_service::IntoService; use super::*; use crate::{ dev::ServiceRequest, http::StatusCode, middleware::{self, Condition, Identity, Logger}, test::{self, call_service, init_service, TestRequest}, web, App, HttpResponse, }; #[actix_rt::test] #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_scope_middleware() { use crate::middleware::Compress; let logger = Logger::default(); let compress = Compress::default(); let srv = init_service( App::new().service( web::scope("app") .wrap(logger) .wrap(Compat::new(compress)) .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_resource_scope_middleware() { use crate::middleware::Compress; let logger = Logger::default(); let compress = Compress::default(); let srv = init_service( App::new().service( web::resource("app/test") .wrap(Compat::new(logger)) .wrap(Compat::new(compress)) .route(web::get().to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_condition_scope_middleware() { let srv = |req: ServiceRequest| { Box::pin( async move { Ok(req.into_response(HttpResponse::InternalServerError().finish())) }, ) }; let logger = Logger::default(); let mw = Condition::new(true, Compat::new(logger)) .new_transform(srv.into_service()) .await .unwrap(); let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[actix_rt::test] async fn compat_noop_is_noop() { let srv = test::ok_service(); let mw = Compat::new(Identity) .new_transform(srv.into_service()) .await .unwrap(); let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.status(), StatusCode::OK); } } actix-web-4.9.0/src/middleware/compress.rs000064400000000000000000000374251046102023000166410ustar 00000000000000//! For middleware documentation, see [`Compress`]. use std::{ future::Future, marker::PhantomData, pin::Pin, task::{Context, Poll}, }; use actix_http::encoding::Encoder; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; use mime::Mime; use once_cell::sync::Lazy; use pin_project_lite::pin_project; use crate::{ body::{EitherBody, MessageBody}, http::{ header::{self, AcceptEncoding, ContentEncoding, Encoding, HeaderValue}, StatusCode, }, service::{ServiceRequest, ServiceResponse}, Error, HttpMessage, HttpResponse, }; /// Middleware for compressing response payloads. /// /// # Encoding Negotiation /// `Compress` will read the `Accept-Encoding` header to negotiate which compression codec to use. /// Payloads are not compressed if the header is not sent. The `compress-*` [feature flags] are also /// considered in this selection process. /// /// # Pre-compressed Payload /// If you are serving some data that is already using a compressed representation (e.g., a gzip /// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate /// `Content-Encoding` header. In addition to preventing double compressing the payload, this header /// is required by the spec when using compressed representations and will inform the client that /// the content should be uncompressed. /// /// However, it is not advised to unconditionally serve encoded representations of content because /// the client may not support it. The [`AcceptEncoding`] typed header has some utilities to help /// perform manual encoding negotiation, if required. When negotiating content encoding, it is also /// required by the spec to send a `Vary: Accept-Encoding` header. /// /// A (naïve) example serving an pre-compressed Gzip file is included below. /// /// # Examples /// To enable automatic payload compression just include `Compress` as a top-level middleware: /// ``` /// use actix_web::{middleware, web, App, HttpResponse}; /// /// let app = App::new() /// .wrap(middleware::Compress::default()) /// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") })); /// ``` /// /// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware: /// ```no_run /// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder}; /// /// async fn index_handler() -> actix_web::Result { /// Ok(actix_files::NamedFile::open_async("./assets/index.html.gz").await? /// .customize() /// .insert_header(header::ContentEncoding::Gzip)) /// } /// /// let app = App::new() /// .wrap(middleware::Compress::default()) /// .default_service(web::to(index_handler)); /// ``` /// /// [feature flags]: ../index.html#crate-features #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct Compress; impl Transform for Compress where B: MessageBody, S: Service, Error = Error>, { type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(CompressMiddleware { service }) } } pub struct CompressMiddleware { service: S, } impl Service for CompressMiddleware where S: Service, Error = Error>, B: MessageBody, { type Response = ServiceResponse>>; type Error = Error; #[allow(clippy::type_complexity)] type Future = Either, Ready>>; actix_service::forward_ready!(service); #[allow(clippy::borrow_interior_mutable_const)] fn call(&self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding let accept_encoding = req.get_header::(); let accept_encoding = match accept_encoding { // missing header; fallback to identity None => { return Either::left(CompressResponse { encoding: Encoding::identity(), fut: self.service.call(req), _phantom: PhantomData, }) } // valid accept-encoding header Some(accept_encoding) => accept_encoding, }; match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) { None => { let mut res = HttpResponse::with_body( StatusCode::NOT_ACCEPTABLE, SUPPORTED_ENCODINGS_STRING.as_str(), ); res.headers_mut() .insert(header::VARY, HeaderValue::from_static("Accept-Encoding")); Either::right(ok(req .into_response(res) .map_into_boxed_body() .map_into_right_body())) } Some(encoding) => Either::left(CompressResponse { fut: self.service.call(req), encoding, _phantom: PhantomData, }), } } } pin_project! { pub struct CompressResponse where S: Service, { #[pin] fut: S::Future, encoding: Encoding, _phantom: PhantomData, } } impl Future for CompressResponse where B: MessageBody, S: Service, Error = Error>, { type Output = Result>>, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.as_mut().project(); match ready!(this.fut.poll(cx)) { Ok(resp) => { let enc = match this.encoding { Encoding::Known(enc) => *enc, Encoding::Unknown(enc) => { unimplemented!("encoding '{enc}' should not be here"); } }; Poll::Ready(Ok(resp.map_body(move |head, body| { let content_type = head.headers.get(header::CONTENT_TYPE); fn default_compress_predicate(content_type: Option<&HeaderValue>) -> bool { match content_type { None => true, Some(hdr) => { match hdr.to_str().ok().and_then(|hdr| hdr.parse::().ok()) { Some(mime) if mime.type_().as_str() == "image" => false, Some(mime) if mime.type_().as_str() == "video" => false, _ => true, } } } } let enc = if default_compress_predicate(content_type) { enc } else { ContentEncoding::Identity }; EitherBody::left(Encoder::response(enc, head, body)) }))) } Err(err) => Poll::Ready(Err(err)), } } } static SUPPORTED_ENCODINGS_STRING: Lazy = Lazy::new(|| { #[allow(unused_mut)] // only unused when no compress features enabled let mut encoding: Vec<&str> = vec![]; #[cfg(feature = "compress-brotli")] { encoding.push("br"); } #[cfg(feature = "compress-gzip")] { encoding.push("gzip"); encoding.push("deflate"); } #[cfg(feature = "compress-zstd")] { encoding.push("zstd"); } assert!( !encoding.is_empty(), "encoding can not be empty unless __compress feature has been explicitly enabled by itself" ); encoding.join(", ") }); static SUPPORTED_ENCODINGS: &[Encoding] = &[ Encoding::identity(), #[cfg(feature = "compress-brotli")] { Encoding::brotli() }, #[cfg(feature = "compress-gzip")] { Encoding::gzip() }, #[cfg(feature = "compress-gzip")] { Encoding::deflate() }, #[cfg(feature = "compress-zstd")] { Encoding::zstd() }, ]; // move cfg(feature) to prevents_double_compressing if more tests are added #[cfg(feature = "compress-gzip")] #[cfg(test)] mod tests { use std::collections::HashSet; use static_assertions::assert_impl_all; use super::*; use crate::{http::header::ContentType, middleware::DefaultHeaders, test, web, App}; const HTML_DATA_PART: &str = "

hello world

) -> Vec { use std::io::Read as _; let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref()); let mut buf = Vec::new(); decoder.read_to_end(&mut buf).unwrap(); buf } #[track_caller] fn assert_successful_res_with_content_type(res: &ServiceResponse, ct: &str) { assert!(res.status().is_success()); assert!( res.headers() .get(header::CONTENT_TYPE) .expect("content-type header should be present") .to_str() .expect("content-type header should be utf-8") .contains(ct), "response's content-type did not match {}", ct ); } #[track_caller] fn assert_successful_gzip_res_with_content_type(res: &ServiceResponse, ct: &str) { assert_successful_res_with_content_type(res, ct); assert_eq!( res.headers() .get(header::CONTENT_ENCODING) .expect("response should be gzip compressed"), "gzip", ); } #[track_caller] fn assert_successful_identity_res_with_content_type(res: &ServiceResponse, ct: &str) { assert_successful_res_with_content_type(res, ct); assert!( res.headers().get(header::CONTENT_ENCODING).is_none(), "response should not be compressed", ); } #[actix_rt::test] async fn prevents_double_compressing() { let app = test::init_service({ App::new() .wrap(Compress::default()) .route( "/single", web::get().to(move || HttpResponse::Ok().body(TEXT_DATA)), ) .service( web::resource("/double") .wrap(Compress::default()) .wrap(DefaultHeaders::new().add(("x-double", "true"))) .route(web::get().to(move || HttpResponse::Ok().body(TEXT_DATA))), ) }) .await; let req = test::TestRequest::default() .uri("/single") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get("x-double"), None); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = test::read_body(res).await; assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes()); let req = test::TestRequest::default() .uri("/double") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get("x-double").unwrap(), "true"); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = test::read_body(res).await; assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes()); } #[actix_rt::test] async fn retains_previously_set_vary_header() { let app = test::init_service({ App::new() .wrap(Compress::default()) .default_service(web::to(move || { HttpResponse::Ok() .insert_header((header::VARY, "x-test")) .body(TEXT_DATA) })) }) .await; let req = test::TestRequest::default() .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); #[allow(clippy::mutable_key_type)] let vary_headers = res.headers().get_all(header::VARY).collect::>(); assert!(vary_headers.contains(&HeaderValue::from_static("x-test"))); assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); } fn configure_predicate_test(cfg: &mut web::ServiceConfig) { cfg.route( "/html", web::get().to(|| { HttpResponse::Ok() .content_type(ContentType::html()) .body(HTML_DATA) }), ) .route( "/image", web::get().to(|| { HttpResponse::Ok() .content_type(ContentType::jpeg()) .body(TEXT_DATA) }), ); } #[actix_rt::test] async fn prevents_compression_jpeg() { let app = test::init_service( App::new() .wrap(Compress::default()) .configure(configure_predicate_test), ) .await; let req = test::TestRequest::with_uri("/html").insert_header((header::ACCEPT_ENCODING, "gzip")); let res = test::call_service(&app, req.to_request()).await; assert_successful_gzip_res_with_content_type(&res, "text/html"); assert_ne!(test::read_body(res).await, HTML_DATA.as_bytes()); let req = test::TestRequest::with_uri("/image").insert_header((header::ACCEPT_ENCODING, "gzip")); let res = test::call_service(&app, req.to_request()).await; assert_successful_identity_res_with_content_type(&res, "image/jpeg"); assert_eq!(test::read_body(res).await, TEXT_DATA.as_bytes()); } #[actix_rt::test] async fn prevents_compression_empty() { let app = test::init_service({ App::new() .wrap(Compress::default()) .default_service(web::to(move || HttpResponse::Ok().finish())) }) .await; let req = test::TestRequest::default() .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); assert!(test::read_body(res).await.is_empty()); } } #[cfg(feature = "compress-brotli")] #[cfg(test)] mod tests_brotli { use super::*; use crate::{test, web, App}; #[actix_rt::test] async fn prevents_compression_empty() { let app = test::init_service({ App::new() .wrap(Compress::default()) .default_service(web::to(move || HttpResponse::Ok().finish())) }) .await; let req = test::TestRequest::default() .insert_header((header::ACCEPT_ENCODING, "br")) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); assert!(test::read_body(res).await.is_empty()); } } actix-web-4.9.0/src/middleware/condition.rs000064400000000000000000000146651046102023000167750ustar 00000000000000//! For middleware documentation, see [`Condition`]. use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use futures_core::{future::LocalBoxFuture, ready}; use futures_util::FutureExt as _; use pin_project_lite::pin_project; use crate::{ body::EitherBody, dev::{Service, ServiceResponse, Transform}, }; /// Middleware for conditionally enabling other middleware. /// /// # Examples /// ``` /// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::App; /// /// let enable_normalize = std::env::var("NORMALIZE_PATH").is_ok(); /// let app = App::new() /// .wrap(Condition::new(enable_normalize, NormalizePath::default())); /// ``` pub struct Condition { transformer: T, enable: bool, } impl Condition { pub fn new(enable: bool, transformer: T) -> Self { Self { transformer, enable, } } } impl Transform for Condition where S: Service, Error = Err> + 'static, T: Transform, Error = Err>, T::Future: 'static, T::InitError: 'static, T::Transform: 'static, { type Response = ServiceResponse>; type Error = Err; type Transform = ConditionMiddleware; type InitError = T::InitError; type Future = LocalBoxFuture<'static, Result>; fn new_transform(&self, service: S) -> Self::Future { if self.enable { let fut = self.transformer.new_transform(service); async move { let wrapped_svc = fut.await?; Ok(ConditionMiddleware::Enable(wrapped_svc)) } .boxed_local() } else { async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local() } } } pub enum ConditionMiddleware { Enable(E), Disable(D), } impl Service for ConditionMiddleware where E: Service, Error = Err>, D: Service, Error = Err>, { type Response = ServiceResponse>; type Error = Err; type Future = ConditionMiddlewareFuture; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { match self { ConditionMiddleware::Enable(service) => service.poll_ready(cx), ConditionMiddleware::Disable(service) => service.poll_ready(cx), } } fn call(&self, req: Req) -> Self::Future { match self { ConditionMiddleware::Enable(service) => ConditionMiddlewareFuture::Enabled { fut: service.call(req), }, ConditionMiddleware::Disable(service) => ConditionMiddlewareFuture::Disabled { fut: service.call(req), }, } } } pin_project! { #[doc(hidden)] #[project = ConditionProj] pub enum ConditionMiddlewareFuture { Enabled { #[pin] fut: E, }, Disabled { #[pin] fut: D, }, } } impl Future for ConditionMiddlewareFuture where E: Future, Err>>, D: Future, Err>>, { type Output = Result>, Err>; #[inline] fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let res = match self.project() { ConditionProj::Enabled { fut } => ready!(fut.poll(cx))?.map_into_left_body(), ConditionProj::Disabled { fut } => ready!(fut.poll(cx))?.map_into_right_body(), }; Poll::Ready(Ok(res)) } } #[cfg(test)] mod tests { use actix_service::IntoService as _; use super::*; use crate::{ body::BoxBody, dev::ServiceRequest, error::Result, http::{ header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, middleware::{self, ErrorHandlerResponse, ErrorHandlers, Identity}, test::{self, TestRequest}, web::Bytes, HttpResponse, }; #[allow(clippy::unnecessary_wraps)] fn render_500(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } #[test] fn compat_with_builtin_middleware() { let _ = Condition::new(true, middleware::Compat::new(Identity)); let _ = Condition::new(true, middleware::Logger::default()); let _ = Condition::new(true, middleware::Compress::default()); let _ = Condition::new(true, middleware::NormalizePath::trim()); let _ = Condition::new(true, middleware::DefaultHeaders::new()); let _ = Condition::new(true, middleware::ErrorHandlers::::new()); let _ = Condition::new(true, middleware::ErrorHandlers::::new()); } #[actix_rt::test] async fn test_handler_enabled() { let srv = |req: ServiceRequest| async move { let resp = HttpResponse::InternalServerError().message_body(String::new())?; Ok(req.into_response(resp)) }; let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Condition::new(true, mw) .new_transform(srv.into_service()) .await .unwrap(); let resp: ServiceResponse, String>> = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } #[actix_rt::test] async fn test_handler_disabled() { let srv = |req: ServiceRequest| async move { let resp = HttpResponse::InternalServerError().message_body(String::new())?; Ok(req.into_response(resp)) }; let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Condition::new(false, mw) .new_transform(srv.into_service()) .await .unwrap(); let resp: ServiceResponse, String>> = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE), None); } } actix-web-4.9.0/src/middleware/default_headers.rs000064400000000000000000000163741046102023000201250ustar 00000000000000//! For middleware documentation, see [`DefaultHeaders`]. use std::{ future::Future, marker::PhantomData, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_http::error::HttpError; use actix_utils::future::{ready, Ready}; use futures_core::ready; use pin_project_lite::pin_project; use crate::{ dev::{Service, Transform}, http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE}, service::{ServiceRequest, ServiceResponse}, Error, }; /// Middleware for setting default response headers. /// /// Headers with the same key that are already set in a response will *not* be overwritten. /// /// # Examples /// ``` /// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// let app = App::new() /// .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) /// .service( /// web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` #[derive(Debug, Clone, Default)] pub struct DefaultHeaders { inner: Rc, } #[derive(Debug, Default)] struct Inner { headers: HeaderMap, } impl DefaultHeaders { /// Constructs an empty `DefaultHeaders` middleware. #[inline] pub fn new() -> DefaultHeaders { DefaultHeaders::default() } /// Adds a header to the default set. /// /// # Panics /// Panics when resolved header name or value is invalid. #[allow(clippy::should_implement_trait)] pub fn add(mut self, header: impl TryIntoHeaderPair) -> Self { // standard header terminology `insert` or `append` for this method would make the behavior // of this middleware less obvious since it only adds the headers if they are not present match header.try_into_pair() { Ok((key, value)) => Rc::get_mut(&mut self.inner) .expect("All default headers must be added before cloning.") .headers .append(key, value), Err(err) => panic!("Invalid header: {}", err.into()), } self } #[doc(hidden)] #[deprecated( since = "4.0.0", note = "Prefer `.add((key, value))`. Will be removed in v5." )] pub fn header(self, key: K, value: V) -> Self where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { self.add(( HeaderName::try_from(key) .map_err(Into::into) .expect("Invalid header name"), HeaderValue::try_from(value) .map_err(Into::into) .expect("Invalid header value"), )) } /// Adds a default *Content-Type* header if response does not contain one. /// /// Default is `application/octet-stream`. pub fn add_content_type(self) -> Self { #[allow(clippy::declare_interior_mutable_const)] const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream"); self.add((CONTENT_TYPE, HV_MIME)) } } impl Transform for DefaultHeaders where S: Service, Error = Error>, S::Future: 'static, { type Response = ServiceResponse; type Error = Error; type Transform = DefaultHeadersMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(DefaultHeadersMiddleware { service, inner: Rc::clone(&self.inner), })) } } pub struct DefaultHeadersMiddleware { service: S, inner: Rc, } impl Service for DefaultHeadersMiddleware where S: Service, Error = Error>, S::Future: 'static, { type Response = ServiceResponse; type Error = Error; type Future = DefaultHeaderFuture; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let inner = Rc::clone(&self.inner); let fut = self.service.call(req); DefaultHeaderFuture { fut, inner, _body: PhantomData, } } } pin_project! { pub struct DefaultHeaderFuture, B> { #[pin] fut: S::Future, inner: Rc, _body: PhantomData, } } impl Future for DefaultHeaderFuture where S: Service, Error = Error>, { type Output = ::Output; #[allow(clippy::borrow_interior_mutable_const)] fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let mut res = ready!(this.fut.poll(cx))?; // set response headers for (key, value) in this.inner.headers.iter() { if !res.headers().contains_key(key) { res.headers_mut().insert(key.clone(), value.clone()); } } Poll::Ready(Ok(res)) } } #[cfg(test)] mod tests { use actix_service::IntoService; use actix_utils::future::ok; use super::*; use crate::{ test::{self, TestRequest}, HttpResponse, }; #[actix_rt::test] async fn adding_default_headers() { let mw = DefaultHeaders::new() .add(("X-TEST", "0001")) .add(("X-TEST-TWO", HeaderValue::from_static("123"))) .new_transform(test::ok_service()) .await .unwrap(); let req = TestRequest::default().to_srv_request(); let res = mw.call(req).await.unwrap(); assert_eq!(res.headers().get("x-test").unwrap(), "0001"); assert_eq!(res.headers().get("x-test-two").unwrap(), "123"); } #[actix_rt::test] async fn no_override_existing() { let req = TestRequest::default().to_srv_request(); let srv = |req: ServiceRequest| { ok(req.into_response( HttpResponse::Ok() .insert_header((CONTENT_TYPE, "0002")) .finish(), )) }; let mw = DefaultHeaders::new() .add((CONTENT_TYPE, "0001")) .new_transform(srv.into_service()) .await .unwrap(); let resp = mw.call(req).await.unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[actix_rt::test] async fn adding_content_type() { let mw = DefaultHeaders::new() .add_content_type() .new_transform(test::ok_service()) .await .unwrap(); let req = TestRequest::default().to_srv_request(); let resp = mw.call(req).await.unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), "application/octet-stream" ); } #[test] #[should_panic] fn invalid_header_name() { DefaultHeaders::new().add((":", "hello")); } #[test] #[should_panic] fn invalid_header_value() { DefaultHeaders::new().add(("x-test", "\n")); } } actix-web-4.9.0/src/middleware/err_handlers.rs000064400000000000000000000536311046102023000174530ustar 00000000000000//! For middleware documentation, see [`ErrorHandlers`]. use std::{ future::Future, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_service::{Service, Transform}; use ahash::AHashMap; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; use crate::{ body::EitherBody, dev::{ServiceRequest, ServiceResponse}, http::StatusCode, Error, Result, }; /// Return type for [`ErrorHandlers`] custom handlers. pub enum ErrorHandlerResponse { /// Immediate HTTP response. Response(ServiceResponse>), /// A future that resolves to an HTTP response. Future(LocalBoxFuture<'static, Result>, Error>>), } type ErrorHandler = dyn Fn(ServiceResponse) -> Result>; type DefaultHandler = Option>>; /// Middleware for registering custom status code based error handlers. /// /// Register handlers with the [`ErrorHandlers::handler()`] method to register a custom error handler /// for a given status code. Handlers can modify existing responses or create completely new ones. /// /// To register a default handler, use the [`ErrorHandlers::default_handler()`] method. This /// handler will be used only if a response has an error status code (400-599) that isn't covered by /// a more specific handler (set with the [`handler()`][ErrorHandlers::handler] method). See examples /// below. /// /// To register a default for only client errors (400-499) or only server errors (500-599), use the /// [`ErrorHandlers::default_handler_client()`] and [`ErrorHandlers::default_handler_server()`] /// methods, respectively. /// /// Any response with a status code that isn't covered by a specific handler or a default handler /// will pass by unchanged by this middleware. /// /// # Examples /// /// Adding a header: /// /// ``` /// use actix_web::{ /// dev::ServiceResponse, /// http::{header, StatusCode}, /// middleware::{ErrorHandlerResponse, ErrorHandlers}, /// web, App, HttpResponse, Result, /// }; /// /// fn add_error_header(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Error"), /// ); /// /// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// let app = App::new() /// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` /// /// Modifying response body: /// /// ``` /// use actix_web::{ /// dev::ServiceResponse, /// http::{header, StatusCode}, /// middleware::{ErrorHandlerResponse, ErrorHandlers}, /// web, App, HttpResponse, Result, /// }; /// /// fn add_error_body(res: ServiceResponse) -> Result> { /// // split service response into request and response components /// let (req, res) = res.into_parts(); /// /// // set body of response to modified body /// let res = res.set_body("An error occurred."); /// /// // modified bodies need to be boxed and placed in the "right" slot /// let res = ServiceResponse::new(req, res) /// .map_into_boxed_body() /// .map_into_right_body(); /// /// Ok(ErrorHandlerResponse::Response(res)) /// } /// /// let app = App::new() /// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_body)) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` /// /// Registering default handler: /// /// ``` /// # use actix_web::{ /// # dev::ServiceResponse, /// # http::{header, StatusCode}, /// # middleware::{ErrorHandlerResponse, ErrorHandlers}, /// # web, App, HttpResponse, Result, /// # }; /// fn add_error_header(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Error"), /// ); /// /// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// fn handle_bad_request(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Bad Request Error"), /// ); /// /// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// // Bad Request errors will hit `handle_bad_request()`, while all other errors will hit /// // `add_error_header()`. The order in which the methods are called is not meaningful. /// let app = App::new() /// .wrap( /// ErrorHandlers::new() /// .default_handler(add_error_header) /// .handler(StatusCode::BAD_REQUEST, handle_bad_request) /// ) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` /// /// You can set default handlers for all client (4xx) or all server (5xx) errors: /// /// ``` /// # use actix_web::{ /// # dev::ServiceResponse, /// # http::{header, StatusCode}, /// # middleware::{ErrorHandlerResponse, ErrorHandlers}, /// # web, App, HttpResponse, Result, /// # }; /// # fn add_error_header(mut res: ServiceResponse) -> Result> { /// # res.response_mut().headers_mut().insert( /// # header::CONTENT_TYPE, /// # header::HeaderValue::from_static("Error"), /// # ); /// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// # } /// # fn handle_bad_request(mut res: ServiceResponse) -> Result> { /// # res.response_mut().headers_mut().insert( /// # header::CONTENT_TYPE, /// # header::HeaderValue::from_static("Bad Request Error"), /// # ); /// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// # } /// // Bad request errors will hit `handle_bad_request()`, other client errors will hit /// // `add_error_header()`, and server errors will pass through unchanged /// let app = App::new() /// .wrap( /// ErrorHandlers::new() /// .default_handler_client(add_error_header) // or .default_handler_server /// .handler(StatusCode::BAD_REQUEST, handle_bad_request) /// ) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` pub struct ErrorHandlers { default_client: DefaultHandler, default_server: DefaultHandler, handlers: Handlers, } type Handlers = Rc>>>; impl Default for ErrorHandlers { fn default() -> Self { ErrorHandlers { default_client: Default::default(), default_server: Default::default(), handlers: Default::default(), } } } impl ErrorHandlers { /// Construct new `ErrorHandlers` instance. pub fn new() -> Self { ErrorHandlers::default() } /// Register error handler for specified status code. pub fn handler(mut self, status: StatusCode, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, { Rc::get_mut(&mut self.handlers) .unwrap() .insert(status, Box::new(handler)); self } /// Register a default error handler. /// /// Any request with a status code that hasn't been given a specific other handler (by calling /// [`.handler()`][ErrorHandlers::handler]) will fall back on this. /// /// Note that this will overwrite any default handlers previously set by calling /// [`default_handler_client()`] or [`.default_handler_server()`], but not any set by calling /// [`.handler()`]. /// /// [`default_handler_client()`]: ErrorHandlers::default_handler_client /// [`.default_handler_server()`]: ErrorHandlers::default_handler_server /// [`.handler()`]: ErrorHandlers::handler pub fn default_handler(self, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, { let handler = Rc::new(handler); let handler2 = Rc::clone(&handler); Self { default_server: Some(handler2), default_client: Some(handler), ..self } } /// Register a handler on which to fall back for client error status codes (400-499). pub fn default_handler_client(self, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, { Self { default_client: Some(Rc::new(handler)), ..self } } /// Register a handler on which to fall back for server error status codes (500-599). pub fn default_handler_server(self, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, { Self { default_server: Some(Rc::new(handler)), ..self } } /// Selects the most appropriate handler for the given status code. /// /// If the `handlers` map has an entry for that status code, that handler is returned. /// Otherwise, fall back on the appropriate default handler. fn get_handler<'a>( status: &StatusCode, default_client: Option<&'a ErrorHandler>, default_server: Option<&'a ErrorHandler>, handlers: &'a Handlers, ) -> Option<&'a ErrorHandler> { handlers .get(status) .map(|h| h.as_ref()) .or_else(|| status.is_client_error().then_some(default_client).flatten()) .or_else(|| status.is_server_error().then_some(default_server).flatten()) } } impl Transform for ErrorHandlers where S: Service, Error = Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = Error; type Transform = ErrorHandlersMiddleware; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_transform(&self, service: S) -> Self::Future { let handlers = Rc::clone(&self.handlers); let default_client = self.default_client.clone(); let default_server = self.default_server.clone(); Box::pin(async move { Ok(ErrorHandlersMiddleware { service, default_client, default_server, handlers, }) }) } } #[doc(hidden)] pub struct ErrorHandlersMiddleware { service: S, default_client: DefaultHandler, default_server: DefaultHandler, handlers: Handlers, } impl Service for ErrorHandlersMiddleware where S: Service, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = Error; type Future = ErrorHandlersFuture; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let handlers = Rc::clone(&self.handlers); let default_client = self.default_client.clone(); let default_server = self.default_server.clone(); let fut = self.service.call(req); ErrorHandlersFuture::ServiceFuture { fut, default_client, default_server, handlers, } } } pin_project! { #[project = ErrorHandlersProj] pub enum ErrorHandlersFuture where Fut: Future, { ServiceFuture { #[pin] fut: Fut, default_client: DefaultHandler, default_server: DefaultHandler, handlers: Handlers, }, ErrorHandlerFuture { fut: LocalBoxFuture<'static, Result>, Error>>, }, } } impl Future for ErrorHandlersFuture where Fut: Future, Error>>, { type Output = Result>, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project() { ErrorHandlersProj::ServiceFuture { fut, default_client, default_server, handlers, } => { let res = ready!(fut.poll(cx))?; let status = res.status(); let handler = ErrorHandlers::get_handler( &status, default_client.as_mut().map(|f| Rc::as_ref(f)), default_server.as_mut().map(|f| Rc::as_ref(f)), handlers, ); match handler { Some(handler) => match handler(res)? { ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)), ErrorHandlerResponse::Future(fut) => { self.as_mut() .set(ErrorHandlersFuture::ErrorHandlerFuture { fut }); self.poll(cx) } }, None => Poll::Ready(Ok(res.map_into_left_body())), } } ErrorHandlersProj::ErrorHandlerFuture { fut } => fut.as_mut().poll(cx), } } } #[cfg(test)] mod tests { use actix_service::IntoService; use actix_utils::future::ok; use bytes::Bytes; use futures_util::FutureExt as _; use super::*; use crate::{ body, http::header::{HeaderValue, CONTENT_TYPE}, test::{self, TestRequest}, }; #[actix_rt::test] async fn add_header_error_handler() { #[allow(clippy::unnecessary_wraps)] fn error_handler(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) .new_transform(srv.into_service()) .await .unwrap(); let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } #[actix_rt::test] async fn add_header_error_handler_async() { #[allow(clippy::unnecessary_wraps)] fn error_handler( mut res: ServiceResponse, ) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Future( ok(res.map_into_left_body()).boxed_local(), )) } let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) .new_transform(srv.into_service()) .await .unwrap(); let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } #[actix_rt::test] async fn changes_body_type() { #[allow(clippy::unnecessary_wraps)] fn error_handler(res: ServiceResponse) -> Result> { let (req, res) = res.into_parts(); let res = res.set_body(Bytes::from("sorry, that's no bueno")); let res = ServiceResponse::new(req, res) .map_into_boxed_body() .map_into_right_body(); Ok(ErrorHandlerResponse::Response(res)) } let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) .new_transform(srv.into_service()) .await .unwrap(); let res = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(test::read_body(res).await, "sorry, that's no bueno"); } #[actix_rt::test] async fn error_thrown() { #[allow(clippy::unnecessary_wraps)] fn error_handler(_res: ServiceResponse) -> Result> { Err(crate::error::ErrorInternalServerError( "error in error handler", )) } let srv = test::status_service(StatusCode::BAD_REQUEST); let mw = ErrorHandlers::new() .handler(StatusCode::BAD_REQUEST, error_handler) .new_transform(srv.into_service()) .await .unwrap(); let err = mw .call(TestRequest::default().to_srv_request()) .await .unwrap_err(); let res = err.error_response(); assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!( body::to_bytes(res.into_body()).await.unwrap(), "error in error handler" ); } #[actix_rt::test] async fn default_error_handler() { #[allow(clippy::unnecessary_wraps)] fn error_handler(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } let make_mw = |status| async move { ErrorHandlers::new() .default_handler(error_handler) .new_transform(test::status_service(status).into_service()) .await .unwrap() }; let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await; let mw_client = make_mw(StatusCode::BAD_REQUEST).await; let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let resp = test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } #[actix_rt::test] async fn default_handlers_separate_client_server() { #[allow(clippy::unnecessary_wraps)] fn error_handler_client(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } #[allow(clippy::unnecessary_wraps)] fn error_handler_server(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0002")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } let make_mw = |status| async move { ErrorHandlers::new() .default_handler_server(error_handler_server) .default_handler_client(error_handler_client) .new_transform(test::status_service(status).into_service()) .await .unwrap() }; let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await; let mw_client = make_mw(StatusCode::BAD_REQUEST).await; let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let resp = test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[actix_rt::test] async fn default_handlers_specialization() { #[allow(clippy::unnecessary_wraps)] fn error_handler_client(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } #[allow(clippy::unnecessary_wraps)] fn error_handler_specific( mut res: ServiceResponse, ) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0003")); Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } let make_mw = |status| async move { ErrorHandlers::new() .default_handler_client(error_handler_client) .handler(StatusCode::UNPROCESSABLE_ENTITY, error_handler_specific) .new_transform(test::status_service(status).into_service()) .await .unwrap() }; let mw_client = make_mw(StatusCode::BAD_REQUEST).await; let mw_specific = make_mw(StatusCode::UNPROCESSABLE_ENTITY).await; let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let resp = test::call_service(&mw_specific, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0003"); } } actix-web-4.9.0/src/middleware/from_fn.rs000064400000000000000000000256231046102023000164310ustar 00000000000000use std::{future::Future, marker::PhantomData, rc::Rc}; use actix_service::boxed::{self, BoxFuture, RcService}; use actix_utils::future::{ready, Ready}; use futures_core::future::LocalBoxFuture; use crate::{ body::MessageBody, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, FromRequest, }; /// Wraps an async function to be used as a middleware. /// /// # Examples /// /// The wrapped function should have the following form: /// /// ``` /// # use actix_web::{ /// # App, Error, /// # body::MessageBody, /// # dev::{ServiceRequest, ServiceResponse, Service as _}, /// # }; /// use actix_web::middleware::{self, Next}; /// /// async fn my_mw( /// req: ServiceRequest, /// next: Next, /// ) -> Result, Error> { /// // pre-processing /// next.call(req).await /// // post-processing /// } /// # App::new().wrap(middleware::from_fn(my_mw)); /// ``` /// /// Then use in an app builder like this: /// /// ``` /// use actix_web::{ /// App, Error, /// dev::{ServiceRequest, ServiceResponse, Service as _}, /// }; /// use actix_web::middleware::from_fn; /// # use actix_web::middleware::Next; /// # async fn my_mw(req: ServiceRequest, next: Next) -> Result, Error> { /// # next.call(req).await /// # } /// /// App::new() /// .wrap(from_fn(my_mw)) /// # ; /// ``` /// /// It is also possible to write a middleware that automatically uses extractors, similar to request /// handlers, by declaring them as the first parameters. As usual, **take care with extractors that /// consume the body stream**, since handlers will no longer be able to read it again without /// putting the body "back" into the request object within your middleware. /// /// ``` /// # use std::collections::HashMap; /// # use actix_web::{ /// # App, Error, /// # body::MessageBody, /// # dev::{ServiceRequest, ServiceResponse}, /// # http::header::{Accept, Date}, /// # web::{Header, Query}, /// # }; /// use actix_web::middleware::Next; /// /// async fn my_extracting_mw( /// accept: Header, /// query: Query>, /// req: ServiceRequest, /// next: Next, /// ) -> Result, Error> { /// // pre-processing /// next.call(req).await /// // post-processing /// } /// # App::new().wrap(actix_web::middleware::from_fn(my_extracting_mw)); pub fn from_fn(mw_fn: F) -> MiddlewareFn { MiddlewareFn { mw_fn: Rc::new(mw_fn), _phantom: PhantomData, } } /// Middleware transform for [`from_fn`]. #[allow(missing_debug_implementations)] pub struct MiddlewareFn { mw_fn: Rc, _phantom: PhantomData, } impl Transform for MiddlewareFn where S: Service, Error = Error> + 'static, F: Fn(ServiceRequest, Next) -> Fut + 'static, Fut: Future, Error>>, B2: MessageBody, { type Response = ServiceResponse; type Error = Error; type Transform = MiddlewareFnService; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(MiddlewareFnService { service: boxed::rc_service(service), mw_fn: Rc::clone(&self.mw_fn), _phantom: PhantomData, })) } } /// Middleware service for [`from_fn`]. #[allow(missing_debug_implementations)] pub struct MiddlewareFnService { service: RcService, Error>, mw_fn: Rc, _phantom: PhantomData<(B, Es)>, } impl Service for MiddlewareFnService where F: Fn(ServiceRequest, Next) -> Fut, Fut: Future, Error>>, B2: MessageBody, { type Response = ServiceResponse; type Error = Error; type Future = Fut; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { (self.mw_fn)( req, Next:: { service: Rc::clone(&self.service), }, ) } } macro_rules! impl_middleware_fn_service { ($($ext_type:ident),*) => { impl Transform for MiddlewareFn where S: Service, Error = Error> + 'static, F: Fn($($ext_type),*, ServiceRequest, Next) -> Fut + 'static, $($ext_type: FromRequest + 'static,)* Fut: Future, Error>> + 'static, B: MessageBody + 'static, B2: MessageBody + 'static, { type Response = ServiceResponse; type Error = Error; type Transform = MiddlewareFnService; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(MiddlewareFnService { service: boxed::rc_service(service), mw_fn: Rc::clone(&self.mw_fn), _phantom: PhantomData, })) } } impl Service for MiddlewareFnService where F: Fn( $($ext_type),*, ServiceRequest, Next ) -> Fut + 'static, $($ext_type: FromRequest + 'static,)* Fut: Future, Error>> + 'static, B2: MessageBody + 'static, { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; forward_ready!(service); #[allow(nonstandard_style)] fn call(&self, mut req: ServiceRequest) -> Self::Future { let mw_fn = Rc::clone(&self.mw_fn); let service = Rc::clone(&self.service); Box::pin(async move { let ($($ext_type,)*) = req.extract::<($($ext_type,)*)>().await?; (mw_fn)($($ext_type),*, req, Next:: { service }).await }) } } }; } impl_middleware_fn_service!(E1); impl_middleware_fn_service!(E1, E2); impl_middleware_fn_service!(E1, E2, E3); impl_middleware_fn_service!(E1, E2, E3, E4); impl_middleware_fn_service!(E1, E2, E3, E4, E5); impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6); impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7); impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8); impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8, E9); /// Wraps the "next" service in the middleware chain. #[allow(missing_debug_implementations)] pub struct Next { service: RcService, Error>, } impl Next { /// Equivalent to `Service::call(self, req)`. pub fn call(&self, req: ServiceRequest) -> >::Future { Service::call(self, req) } } impl Service for Next { type Response = ServiceResponse; type Error = Error; type Future = BoxFuture>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { self.service.call(req) } } #[cfg(test)] mod tests { use super::*; use crate::{ http::header::{self, HeaderValue}, middleware::{Compat, Logger}, test, web, App, HttpResponse, }; async fn noop(req: ServiceRequest, next: Next) -> Result, Error> { next.call(req).await } async fn add_res_header( req: ServiceRequest, next: Next, ) -> Result, Error> { let mut res = next.call(req).await?; res.headers_mut() .insert(header::WARNING, HeaderValue::from_static("42")); Ok(res) } async fn mutate_body_type( req: ServiceRequest, next: Next, ) -> Result, Error> { let res = next.call(req).await?; Ok(res.map_into_left_body::<()>()) } struct MyMw(bool); impl MyMw { async fn mw_cb( &self, req: ServiceRequest, next: Next, ) -> Result, Error> { let mut res = match self.0 { true => req.into_response("short-circuited").map_into_right_body(), false => next.call(req).await?.map_into_left_body(), }; res.headers_mut() .insert(header::WARNING, HeaderValue::from_static("42")); Ok(res) } pub fn into_middleware( self, ) -> impl Transform< S, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > where S: Service, Error = Error> + 'static, B: MessageBody + 'static, { let this = Rc::new(self); from_fn(move |req, next| { let this = Rc::clone(&this); async move { Self::mw_cb(&this, req, next).await } }) } } #[actix_rt::test] async fn compat_compat() { let _ = App::new().wrap(Compat::new(from_fn(noop))); let _ = App::new().wrap(Compat::new(from_fn(mutate_body_type))); } #[actix_rt::test] async fn permits_different_in_and_out_body_types() { let app = test::init_service( App::new() .wrap(from_fn(mutate_body_type)) .wrap(from_fn(add_res_header)) .wrap(Logger::default()) .wrap(from_fn(noop)) .default_service(web::to(HttpResponse::NotFound)), ) .await; let req = test::TestRequest::default().to_request(); let res = test::call_service(&app, req).await; assert!(res.headers().contains_key(header::WARNING)); } #[actix_rt::test] async fn closure_capture_and_return_from_fn() { let app = test::init_service( App::new() .wrap(Logger::default()) .wrap(MyMw(true).into_middleware()) .wrap(Logger::default()), ) .await; let req = test::TestRequest::default().to_request(); let res = test::call_service(&app, req).await; assert!(res.headers().contains_key(header::WARNING)); } } actix-web-4.9.0/src/middleware/identity.rs000064400000000000000000000020131046102023000166200ustar 00000000000000//! A no-op middleware. See [Noop] for docs. use actix_utils::future::{ready, Ready}; use crate::dev::{forward_ready, Service, Transform}; /// A no-op middleware that passes through request and response untouched. #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct Identity; impl, Req> Transform for Identity { type Response = S::Response; type Error = S::Error; type Transform = IdentityMiddleware; type InitError = (); type Future = Ready>; #[inline] fn new_transform(&self, service: S) -> Self::Future { ready(Ok(IdentityMiddleware { service })) } } #[doc(hidden)] pub struct IdentityMiddleware { service: S, } impl, Req> Service for IdentityMiddleware { type Response = S::Response; type Error = S::Error; type Future = S::Future; forward_ready!(service); #[inline] fn call(&self, req: Req) -> Self::Future { self.service.call(req) } } actix-web-4.9.0/src/middleware/logger.rs000064400000000000000000000770441046102023000162660ustar 00000000000000//! For middleware documentation, see [`Logger`]. use std::{ borrow::Cow, collections::HashSet, env, fmt::{self, Display as _}, future::Future, marker::PhantomData, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_service::{Service, Transform}; use actix_utils::future::{ready, Ready}; use bytes::Bytes; use futures_core::ready; use log::{debug, warn}; use pin_project_lite::pin_project; #[cfg(feature = "unicode")] use regex::Regex; #[cfg(not(feature = "unicode"))] use regex_lite::Regex; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ body::{BodySize, MessageBody}, http::header::HeaderName, service::{ServiceRequest, ServiceResponse}, Error, Result, }; /// Middleware for logging request and response summaries to the terminal. /// /// This middleware uses the `log` crate to output information. Enable `log`'s output for the /// "actix_web" scope using [`env_logger`](https://docs.rs/env_logger) or similar crate. /// /// # Default Format /// The [`default`](Logger::default) Logger uses the following format: /// /// ```plain /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// /// Example Output: /// 127.0.0.1:54278 "GET /test HTTP/1.1" 404 20 "-" "HTTPie/2.2.0" 0.001074 /// ``` /// /// # Examples /// ``` /// use actix_web::{middleware::Logger, App}; /// /// // access logs are printed with the INFO level so ensure it is enabled by default /// env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); /// /// let app = App::new() /// // .wrap(Logger::default()) /// .wrap(Logger::new("%a %{User-Agent}i")); /// ``` /// /// # Format /// Variable | Description /// -------- | ----------- /// `%%` | The percent sign /// `%a` | Peer IP address (or IP address of reverse proxy if used) /// `%t` | Time when the request started processing (in RFC 3339 format) /// `%r` | First line of request (Example: `GET /test HTTP/1.1`) /// `%s` | Response status code /// `%b` | Size of response in bytes, including HTTP headers /// `%T` | Time taken to serve the request, in seconds to 6 decimal places /// `%D` | Time taken to serve the request, in milliseconds /// `%U` | Request URL /// `%{r}a` | "Real IP" remote address **\*** /// `%{FOO}i` | `request.headers["FOO"]` /// `%{FOO}o` | `response.headers["FOO"]` /// `%{FOO}e` | `env_var["FOO"]` /// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO" /// `%{FOO}xo` | [Custom response replacement](Logger::custom_response_replace) labelled "FOO" /// /// # Security /// **\*** "Real IP" remote address is calculated using /// [`ConnectionInfo::realip_remote_addr()`](crate::dev::ConnectionInfo::realip_remote_addr()) /// /// If you use this value, ensure that all requests come from trusted hosts. Otherwise, it is /// trivial for the remote client to falsify their source IP address. #[derive(Debug)] pub struct Logger(Rc); #[derive(Debug, Clone)] struct Inner { format: Format, exclude: HashSet, exclude_regex: Vec, log_target: Cow<'static, str>, } impl Logger { /// Create `Logger` middleware with the specified `format`. pub fn new(format: &str) -> Logger { Logger(Rc::new(Inner { format: Format::new(format), exclude: HashSet::new(), exclude_regex: Vec::new(), log_target: Cow::Borrowed(module_path!()), })) } /// Ignore and do not log access info for specified path. pub fn exclude>(mut self, path: T) -> Self { Rc::get_mut(&mut self.0) .unwrap() .exclude .insert(path.into()); self } /// Ignore and do not log access info for paths that match regex. pub fn exclude_regex>(mut self, path: T) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); inner.exclude_regex.push(Regex::new(&path.into()).unwrap()); self } /// Sets the logging target to `target`. /// /// By default, the log target is `module_path!()` of the log call location. In our case, that /// would be `actix_web::middleware::logger`. /// /// # Examples /// Using `.log_target("http_log")` would have this effect on request logs: /// ```diff /// - [2015-10-21T07:28:00Z INFO actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985 /// + [2015-10-21T07:28:00Z INFO http_log] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985 /// ^^^^^^^^ /// ``` pub fn log_target(mut self, target: impl Into>) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); inner.log_target = target.into(); self } /// Register a function that receives a ServiceRequest and returns a String for use in the /// log line. The label passed as the first argument should match a replacement substring in /// the logger format like `%{label}xi`. /// /// It is convention to print "-" to indicate no output instead of an empty string. /// /// # Examples /// ``` /// # use actix_web::http::{header::HeaderValue}; /// # use actix_web::middleware::Logger; /// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() } /// Logger::new("example %{JWT_ID}xi") /// .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization"))); /// ``` pub fn custom_request_replace( mut self, label: &str, f: impl Fn(&ServiceRequest) -> String + 'static, ) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); let ft = inner.format.0.iter_mut().find( |ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label), ); if let Some(FormatText::CustomRequest(_, request_fn)) = ft { // replace into None or previously registered fn using same label request_fn.replace(CustomRequestFn { inner_fn: Rc::new(f), }); } else { // non-printed request replacement function diagnostic debug!( "Attempted to register custom request logging function for nonexistent label: {}", label ); } self } /// Register a function that receives a `ServiceResponse` and returns a string for use in the /// log line. /// /// The label passed as the first argument should match a replacement substring in /// the logger format like `%{label}xo`. /// /// It is convention to print "-" to indicate no output instead of an empty string. /// /// The replacement function does not have access to the response body. /// /// # Examples /// ``` /// # use actix_web::{dev::ServiceResponse, middleware::Logger}; /// fn log_if_error(res: &ServiceResponse) -> String { /// if res.status().as_u16() >= 400 { /// "ERROR".to_string() /// } else { /// "-".to_string() /// } /// } /// /// Logger::new("example %{ERROR_STATUS}xo") /// .custom_response_replace("ERROR_STATUS", |res| log_if_error(res) ); /// ``` pub fn custom_response_replace( mut self, label: &str, f: impl Fn(&ServiceResponse) -> String + 'static, ) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); let ft = inner.format.0.iter_mut().find( |ft| matches!(ft, FormatText::CustomResponse(unit_label, _) if label == unit_label), ); if let Some(FormatText::CustomResponse(_, res_fn)) = ft { *res_fn = Some(CustomResponseFn { inner_fn: Rc::new(f), }); } else { debug!( "Attempted to register custom response logging function for non-existent label: {}", label ); } self } } impl Default for Logger { /// Create `Logger` middleware with format: /// /// ```plain /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// ``` fn default() -> Logger { Logger(Rc::new(Inner { format: Format::default(), exclude: HashSet::new(), exclude_regex: Vec::new(), log_target: Cow::Borrowed(module_path!()), })) } } impl Transform for Logger where S: Service, Error = Error>, B: MessageBody, { type Response = ServiceResponse>; type Error = Error; type Transform = LoggerMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { for unit in &self.0.format.0 { if let FormatText::CustomRequest(label, None) = unit { warn!( "No custom request replacement function was registered for label: {}", label ); } if let FormatText::CustomResponse(label, None) = unit { warn!( "No custom response replacement function was registered for label: {}", label ); } } ready(Ok(LoggerMiddleware { service, inner: Rc::clone(&self.0), })) } } /// Logger middleware service. pub struct LoggerMiddleware { inner: Rc, service: S, } impl Service for LoggerMiddleware where S: Service, Error = Error>, B: MessageBody, { type Response = ServiceResponse>; type Error = Error; type Future = LoggerResponse; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let excluded = self.inner.exclude.contains(req.path()) || self .inner .exclude_regex .iter() .any(|r| r.is_match(req.path())); if excluded { LoggerResponse { fut: self.service.call(req), format: None, time: OffsetDateTime::now_utc(), log_target: Cow::Borrowed(""), _phantom: PhantomData, } } else { let now = OffsetDateTime::now_utc(); let mut format = self.inner.format.clone(); for unit in &mut format.0 { unit.render_request(now, &req); } LoggerResponse { fut: self.service.call(req), format: Some(format), time: now, log_target: self.inner.log_target.clone(), _phantom: PhantomData, } } } } pin_project! { pub struct LoggerResponse where B: MessageBody, S: Service, { #[pin] fut: S::Future, time: OffsetDateTime, format: Option, log_target: Cow<'static, str>, _phantom: PhantomData, } } impl Future for LoggerResponse where B: MessageBody, S: Service, Error = Error>, { type Output = Result>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let res = match ready!(this.fut.poll(cx)) { Ok(res) => res, Err(err) => return Poll::Ready(Err(err)), }; if let Some(error) = res.response().error() { debug!("Error in response: {:?}", error); } let res = if let Some(ref mut format) = this.format { // to avoid polluting all the Logger types with the body parameter we swap the body // out temporarily since it's not usable in custom response functions anyway let (req, res) = res.into_parts(); let (res, body) = res.into_parts(); let temp_res = ServiceResponse::new(req, res.map_into_boxed_body()); for unit in &mut format.0 { unit.render_response(&temp_res); } // re-construct original service response let (req, res) = temp_res.into_parts(); ServiceResponse::new(req, res.set_body(body)) } else { res }; let time = *this.time; let format = this.format.take(); let log_target = this.log_target.clone(); Poll::Ready(Ok(res.map_body(move |_, body| StreamLog { body, time, format, size: 0, log_target, }))) } } pin_project! { pub struct StreamLog { #[pin] body: B, format: Option, size: usize, time: OffsetDateTime, log_target: Cow<'static, str>, } impl PinnedDrop for StreamLog { fn drop(this: Pin<&mut Self>) { if let Some(ref format) = this.format { let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, this.size, this.time)?; } Ok(()) }; log::info!( target: this.log_target.as_ref(), "{}", FormatDisplay(&render) ); } } } } impl MessageBody for StreamLog { type Error = B::Error; #[inline] fn size(&self) -> BodySize { self.body.size() } fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { let this = self.project(); match ready!(this.body.poll_next(cx)) { Some(Ok(chunk)) => { *this.size += chunk.len(); Poll::Ready(Some(Ok(chunk))) } Some(Err(err)) => Poll::Ready(Some(Err(err))), None => Poll::Ready(None), } } } /// A formatting style for the `Logger` consisting of multiple concatenated `FormatText` items. #[derive(Debug, Clone)] struct Format(Vec); impl Default for Format { /// Return the default formatting style for the `Logger`: fn default() -> Format { Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) } } impl Format { /// Create a `Format` from a format string. /// /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|x[io])|[%atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); for cap in fmt.captures_iter(s) { let m = cap.get(0).unwrap(); let pos = m.start(); if idx != pos { results.push(FormatText::Str(s[idx..pos].to_owned())); } idx = m.end(); if let Some(key) = cap.get(2) { results.push(match cap.get(3).unwrap().as_str() { "a" => { if key.as_str() == "r" { FormatText::RealIpRemoteAddr } else { unreachable!("regex and code mismatch") } } "i" => FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()), "o" => FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()), "e" => FormatText::EnvironHeader(key.as_str().to_owned()), "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None), "xo" => FormatText::CustomResponse(key.as_str().to_owned(), None), _ => unreachable!(), }) } else { let m = cap.get(1).unwrap(); results.push(match m.as_str() { "%" => FormatText::Percent, "a" => FormatText::RemoteAddr, "t" => FormatText::RequestTime, "r" => FormatText::RequestLine, "s" => FormatText::ResponseStatus, "b" => FormatText::ResponseSize, "U" => FormatText::UrlPath, "T" => FormatText::Time, "D" => FormatText::TimeMillis, _ => FormatText::Str(m.as_str().to_owned()), }); } } if idx != s.len() { results.push(FormatText::Str(s[idx..].to_owned())); } Format(results) } } /// A string of text to be logged. /// /// This is either one of the data fields supported by the `Logger`, or a custom `String`. #[non_exhaustive] #[derive(Debug, Clone)] enum FormatText { Str(String), Percent, RequestLine, RequestTime, ResponseStatus, ResponseSize, Time, TimeMillis, RemoteAddr, RealIpRemoteAddr, UrlPath, RequestHeader(HeaderName), ResponseHeader(HeaderName), EnvironHeader(String), CustomRequest(String, Option), CustomResponse(String, Option), } #[derive(Clone)] struct CustomRequestFn { inner_fn: Rc String>, } impl CustomRequestFn { fn call(&self, req: &ServiceRequest) -> String { (self.inner_fn)(req) } } impl fmt::Debug for CustomRequestFn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("custom_request_fn") } } #[derive(Clone)] struct CustomResponseFn { inner_fn: Rc String>, } impl CustomResponseFn { fn call(&self, res: &ServiceResponse) -> String { (self.inner_fn)(res) } } impl fmt::Debug for CustomResponseFn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("custom_response_fn") } } impl FormatText { fn render( &self, fmt: &mut fmt::Formatter<'_>, size: usize, entry_time: OffsetDateTime, ) -> Result<(), fmt::Error> { match self { FormatText::Str(ref string) => fmt.write_str(string), FormatText::Percent => "%".fmt(fmt), FormatText::ResponseSize => size.fmt(fmt), FormatText::Time => { let rt = OffsetDateTime::now_utc() - entry_time; let rt = rt.as_seconds_f64(); fmt.write_fmt(format_args!("{:.6}", rt)) } FormatText::TimeMillis => { let rt = OffsetDateTime::now_utc() - entry_time; let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0; fmt.write_fmt(format_args!("{:.6}", rt)) } FormatText::EnvironHeader(ref name) => { if let Ok(val) = env::var(name) { fmt.write_fmt(format_args!("{}", val)) } else { "-".fmt(fmt) } } _ => Ok(()), } } fn render_response(&mut self, res: &ServiceResponse) { match self { FormatText::ResponseStatus => { *self = FormatText::Str(format!("{}", res.status().as_u16())) } FormatText::ResponseHeader(ref name) => { let s = if let Some(val) = res.headers().get(name) { val.to_str().unwrap_or("-") } else { "-" }; *self = FormatText::Str(s.to_string()) } FormatText::CustomResponse(_, res_fn) => { let text = match res_fn { Some(res_fn) => FormatText::Str(res_fn.call(res)), None => FormatText::Str("-".to_owned()), }; *self = text; } _ => {} } } fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) { match self { FormatText::RequestLine => { *self = if req.query_string().is_empty() { FormatText::Str(format!( "{} {} {:?}", req.method(), req.path(), req.version() )) } else { FormatText::Str(format!( "{} {}?{} {:?}", req.method(), req.path(), req.query_string(), req.version() )) }; } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { val.to_str().unwrap_or("-") } else { "-" }; *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { let s = if let Some(peer) = req.connection_info().peer_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) }; *self = s; } FormatText::RealIpRemoteAddr => { let s = if let Some(remote) = req.connection_info().realip_remote_addr() { FormatText::Str(remote.to_string()) } else { FormatText::Str("-".to_string()) }; *self = s; } FormatText::CustomRequest(_, request_fn) => { let s = match request_fn { Some(f) => FormatText::Str(f.call(req)), None => FormatText::Str("-".to_owned()), }; *self = s; } _ => {} } } } /// Converter to get a String from something that writes to a Formatter. pub(crate) struct FormatDisplay<'a>(&'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>); impl<'a> fmt::Display for FormatDisplay<'a> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { (self.0)(fmt) } } #[cfg(test)] mod tests { use actix_service::IntoService; use actix_utils::future::ok; use super::*; use crate::{ http::{header, StatusCode}, test::{self, TestRequest}, HttpResponse, }; #[actix_rt::test] async fn test_logger() { let srv = |req: ServiceRequest| { ok(req.into_response( HttpResponse::build(StatusCode::OK) .insert_header(("X-Test", "ttt")) .finish(), )) }; let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); let srv = logger.new_transform(srv.into_service()).await.unwrap(); let req = TestRequest::default() .insert_header(( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), )) .to_srv_request(); let _res = srv.call(req).await; } #[actix_rt::test] async fn test_logger_exclude_regex() { let srv = |req: ServiceRequest| { ok(req.into_response( HttpResponse::build(StatusCode::OK) .insert_header(("X-Test", "ttt")) .finish(), )) }; let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w"); let srv = logger.new_transform(srv.into_service()).await.unwrap(); let req = TestRequest::default() .insert_header(( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), )) .to_srv_request(); let _res = srv.call(req).await.unwrap(); } #[actix_rt::test] async fn test_escape_percent() { let mut format = Format::new("%%{r}a"); let req = TestRequest::default() .insert_header(( header::FORWARDED, header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), )) .to_srv_request(); let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } let req = TestRequest::default().to_http_request(); let res = ServiceResponse::new(req, HttpResponse::Ok().finish()); for unit in &mut format.0 { unit.render_response(&res); } let entry_time = OffsetDateTime::now_utc(); let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; } Ok(()) }; let s = format!("{}", FormatDisplay(&render)); assert_eq!(s, "%{r}a"); } #[actix_rt::test] async fn test_url_path() { let mut format = Format::new("%T %U"); let req = TestRequest::default() .insert_header(( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), )) .uri("/test/route/yeah") .to_srv_request(); let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } let req = TestRequest::default().to_http_request(); let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish()); for unit in &mut format.0 { unit.render_response(&res); } let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, now)?; } Ok(()) }; let s = format!("{}", FormatDisplay(&render)); assert!(s.contains("/test/route/yeah")); } #[actix_rt::test] async fn test_default_format() { let mut format = Format::default(); let req = TestRequest::default() .insert_header(( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), )) .peer_addr("127.0.0.1:8081".parse().unwrap()) .to_srv_request(); let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } let req = TestRequest::default().to_http_request(); let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish()); for unit in &mut format.0 { unit.render_response(&res); } let entry_time = OffsetDateTime::now_utc(); let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; } Ok(()) }; let s = format!("{}", FormatDisplay(&render)); assert!(s.contains("GET / HTTP/1.1")); assert!(s.contains("127.0.0.1")); assert!(s.contains("200 1024")); assert!(s.contains("ACTIX-WEB")); } #[actix_rt::test] async fn test_request_time_format() { let mut format = Format::new("%t"); let req = TestRequest::default().to_srv_request(); let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } let req = TestRequest::default().to_http_request(); let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish()); for unit in &mut format.0 { unit.render_response(&res); } let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, now)?; } Ok(()) }; let s = format!("{}", FormatDisplay(&render)); assert!(s.contains(&now.format(&Rfc3339).unwrap())); } #[actix_rt::test] async fn test_remote_addr_format() { let mut format = Format::new("%{r}a"); let req = TestRequest::default() .insert_header(( header::FORWARDED, header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), )) .to_srv_request(); let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } let req = TestRequest::default().to_http_request(); let res = ServiceResponse::new(req, HttpResponse::Ok().finish()); for unit in &mut format.0 { unit.render_response(&res); } let entry_time = OffsetDateTime::now_utc(); let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; } Ok(()) }; let s = format!("{}", FormatDisplay(&render)); assert!(s.contains("192.0.2.60")); } #[actix_rt::test] async fn test_custom_closure_req_log() { let mut logger = Logger::new("test %{CUSTOM}xi") .custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String { String::from("custom_log") }); let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone(); let label = match &unit { FormatText::CustomRequest(label, _) => label, ft => panic!("expected CustomRequest, found {:?}", ft), }; assert_eq!(label, "CUSTOM"); let req = TestRequest::default().to_srv_request(); let now = OffsetDateTime::now_utc(); unit.render_request(now, &req); let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now); let log_output = FormatDisplay(&render).to_string(); assert_eq!(log_output, "custom_log"); } #[actix_rt::test] async fn test_custom_closure_response_log() { let mut logger = Logger::new("test %{CUSTOM}xo").custom_response_replace( "CUSTOM", |res: &ServiceResponse| -> String { if res.status().as_u16() == 200 { String::from("custom_log") } else { String::from("-") } }, ); let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone(); let label = match &unit { FormatText::CustomResponse(label, _) => label, ft => panic!("expected CustomResponse, found {:?}", ft), }; assert_eq!(label, "CUSTOM"); let req = TestRequest::default().to_http_request(); let resp_ok = ServiceResponse::new(req, HttpResponse::Ok().finish()); let now = OffsetDateTime::now_utc(); unit.render_response(&resp_ok); let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now); let log_output = FormatDisplay(&render).to_string(); assert_eq!(log_output, "custom_log"); } #[actix_rt::test] async fn test_closure_logger_in_middleware() { let captured = "custom log replacement"; let logger = Logger::new("%{CUSTOM}xi") .custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String { captured.to_owned() }); let srv = logger.new_transform(test::ok_service()).await.unwrap(); let req = TestRequest::default().to_srv_request(); srv.call(req).await.unwrap(); } } actix-web-4.9.0/src/middleware/mod.rs000064400000000000000000000265711046102023000155650ustar 00000000000000//! A collection of common middleware. //! //! # What Is Middleware? //! //! Actix Web's middleware system allows us to add additional behavior to request/response //! processing. Middleware can hook into incoming request and outgoing response processes, enabling //! us to modify requests and responses as well as halt request processing to return a response //! early. //! //! Typically, middleware is involved in the following actions: //! //! - Pre-process the request (e.g., [normalizing paths](NormalizePath)) //! - Post-process a response (e.g., [logging][Logger]) //! - Modify application state (through [`ServiceRequest`][crate::dev::ServiceRequest]) //! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.) //! //! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or //! [`Resource`](crate::Resource) and executed in opposite order as registration. //! //! # Simple Middleware //! //! In many cases, you can model your middleware as an async function via the [`from_fn()`] helper //! that provides a natural interface for implementing your desired behaviors. //! //! ``` //! # use actix_web::{ //! # App, Error, //! # body::MessageBody, //! # dev::{ServiceRequest, ServiceResponse, Service as _}, //! # }; //! use actix_web::middleware::{self, Next}; //! //! async fn my_mw( //! req: ServiceRequest, //! next: Next, //! ) -> Result, Error> { //! // pre-processing //! //! // invoke the wrapped middleware or service //! let res = next.call(req).await?; //! //! // post-processing //! //! Ok(res) //! } //! //! App::new() //! .wrap(middleware::from_fn(my_mw)); //! ``` //! //! ## Complex Middleware //! //! In the more general ase, a middleware is a pair of types that implements the [`Service`] trait //! and [`Transform`] trait, respectively. The [`new_transform`] and [`call`] methods must return a //! [`Future`], though it can often be [an immediately-ready one](actix_utils::future::Ready). //! //! All the built-in middleware use this pattern with pairs of builder (`Transform`) + //! implementation (`Service`) types. //! //! # Ordering //! //! ``` //! # use actix_web::{web, middleware, get, App, Responder}; //! # //! # // some basic types to make sure this compiles //! # type ExtractorA = web::Json; //! # type ExtractorB = ExtractorA; //! #[get("/")] //! async fn service(a: ExtractorA, b: ExtractorB) -> impl Responder { "Hello, World!" } //! //! # fn main() { //! # // These aren't snake_case, because they are supposed to be unit structs. //! # type MiddlewareA = middleware::Compress; //! # type MiddlewareB = middleware::Compress; //! # type MiddlewareC = middleware::Compress; //! let app = App::new() //! .wrap(MiddlewareA::default()) //! .wrap(MiddlewareB::default()) //! .wrap(MiddlewareC::default()) //! .service(service); //! # } //! ``` //! //! ```plain //! Request //! ⭣ //! ╭────────────────────┼────╮ //! │ MiddlewareC │ │ //! │ ╭──────────────────┼───╮│ //! │ │ MiddlewareB │ ││ //! │ │ ╭────────────────┼──╮││ //! │ │ │ MiddlewareA │ │││ //! │ │ │ ╭──────────────┼─╮│││ //! │ │ │ │ ExtractorA │ ││││ //! │ │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┼┈┤│││ //! │ │ │ │ ExtractorB │ ││││ //! │ │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┼┈┤│││ //! │ │ │ │ service │ ││││ //! │ │ │ ╰──────────────┼─╯│││ //! │ │ ╰────────────────┼──╯││ //! │ ╰──────────────────┼───╯│ //! ╰────────────────────┼────╯ //! ⭣ //! Response //! ``` //! The request _first_ gets processed by the middleware specified _last_ - `MiddlewareC`. It passes //! the request (possibly a modified one) to the next middleware - `MiddlewareB` - _or_ directly //! responds to the request (e.g. when the request was invalid or an error occurred). `MiddlewareB` //! processes the request as well and passes it to `MiddlewareA`, which then passes it to the //! [`Service`]. In the [`Service`], the extractors will run first. They don't pass the request on, //! but only view it (see [`FromRequest`]). After the [`Service`] responds to the request, the //! response is passed back through `MiddlewareA`, `MiddlewareB`, and `MiddlewareC`. //! //! As you register middleware using [`wrap`][crate::App::wrap] and [`wrap_fn`][crate::App::wrap_fn] //! in the [`App`] builder, imagine wrapping layers around an inner [`App`]. The first middleware //! layer exposed to a Request is the outermost layer (i.e., the _last_ registered in the builder //! chain, in the example above: `MiddlewareC`). Consequently, the _first_ middleware registered in //! the builder chain is the _last_ to start executing during request processing (`MiddlewareA`). //! Ordering is less obvious when wrapped services also have middleware applied. In this case, //! middleware are run in reverse order for [`App`] _and then_ in reverse order for the wrapped //! service. //! //! # Middleware Traits //! //! ## `Transform` //! //! The [`Transform`] trait is the builder for the actual [`Service`]s that handle the requests. All //! the middleware you pass to the `wrap` methods implement this trait. During construction, each //! thread assembles a chain of [`Service`]s by calling [`new_transform`] and passing the next //! [`Service`] (`S`) in the chain. The created [`Service`] handles requests of type `Req`. //! //! In the example from the [ordering](#ordering) section, the chain would be: //! //! ```plain //! MiddlewareCService { //! next: MiddlewareBService { //! next: MiddlewareAService { ... } //! } //! } //! ``` //! //! ## `Service` //! //! A [`Service`] `S` represents an asynchronous operation that turns a request of type `Req` into a //! response of type [`S::Response`](crate::dev::Service::Response) or an error of type //! [`S::Error`](crate::dev::Service::Error). You can think of the service of being roughly: //! //! ```ignore //! async fn(&self, req: Req) -> Result //! ``` //! //! In most cases the [`Service`] implementation will, at some point, call the wrapped [`Service`] //! in its [`call`] implementation. //! //! Note that the [`Service`]s created by [`new_transform`] don't need to be [`Send`] or [`Sync`]. //! //! # Example //! //! ``` //! use std::{future::{ready, Ready, Future}, pin::Pin}; //! //! use actix_web::{ //! dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, //! web, Error, //! # App //! }; //! //! pub struct SayHi; //! //! // `S` - type of the next service //! // `B` - type of response's body //! impl Transform for SayHi //! where //! S: Service, Error = Error>, //! S::Future: 'static, //! B: 'static, //! { //! type Response = ServiceResponse; //! type Error = Error; //! type InitError = (); //! type Transform = SayHiMiddleware; //! type Future = Ready>; //! //! fn new_transform(&self, service: S) -> Self::Future { //! ready(Ok(SayHiMiddleware { service })) //! } //! } //! //! pub struct SayHiMiddleware { //! /// The next service to call //! service: S, //! } //! //! // This future doesn't have the requirement of being `Send`. //! // See: futures_util::future::LocalBoxFuture //! type LocalBoxFuture = Pin + 'static>>; //! //! // `S`: type of the wrapped service //! // `B`: type of the body - try to be generic over the body where possible //! impl Service for SayHiMiddleware //! where //! S: Service, Error = Error>, //! S::Future: 'static, //! B: 'static, //! { //! type Response = ServiceResponse; //! type Error = Error; //! type Future = LocalBoxFuture>; //! //! // This service is ready when its next service is ready //! forward_ready!(service); //! //! fn call(&self, req: ServiceRequest) -> Self::Future { //! println!("Hi from start. You requested: {}", req.path()); //! //! // A more complex middleware, could return an error or an early response here. //! //! let fut = self.service.call(req); //! //! Box::pin(async move { //! let res = fut.await?; //! //! println!("Hi from response"); //! Ok(res) //! }) //! } //! } //! //! # fn main() { //! let app = App::new() //! .wrap(SayHi) //! .route("/", web::get().to(|| async { "Hello, middleware!" })); //! # } //! ``` //! //! [`Future`]: std::future::Future //! [`App`]: crate::App //! [`FromRequest`]: crate::FromRequest //! [`Service`]: crate::dev::Service //! [`Transform`]: crate::dev::Transform //! [`call`]: crate::dev::Service::call() //! [`new_transform`]: crate::dev::Transform::new_transform() //! [`from_fn`]: crate mod compat; #[cfg(feature = "__compress")] mod compress; mod condition; mod default_headers; mod err_handlers; mod from_fn; mod identity; mod logger; mod normalize; #[cfg(feature = "__compress")] pub use self::compress::Compress; pub use self::{ compat::Compat, condition::Condition, default_headers::DefaultHeaders, err_handlers::{ErrorHandlerResponse, ErrorHandlers}, from_fn::{from_fn, Next}, identity::Identity, logger::Logger, normalize::{NormalizePath, TrailingSlash}, }; #[cfg(test)] mod tests { use super::*; use crate::{http::StatusCode, App}; #[test] fn common_combinations() { // ensure there's no reason that the built-in middleware cannot compose let _ = App::new() .wrap(Compat::new(Logger::default())) .wrap(Condition::new(true, DefaultHeaders::new())) .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2"))) .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) })) .wrap(Logger::default()) .wrap(NormalizePath::new(TrailingSlash::Trim)); let _ = App::new() .wrap(NormalizePath::new(TrailingSlash::Trim)) .wrap(Logger::default()) .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) })) .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2"))) .wrap(Condition::new(true, DefaultHeaders::new())) .wrap(Compat::new(Logger::default())); #[cfg(feature = "__compress")] { let _ = App::new().wrap(Compress::default()).wrap(Logger::default()); let _ = App::new().wrap(Logger::default()).wrap(Compress::default()); let _ = App::new().wrap(Compat::new(Compress::default())); let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default()))); } } } actix-web-4.9.0/src/middleware/normalize.rs000064400000000000000000000410751046102023000170020ustar 00000000000000//! For middleware documentation, see [`NormalizePath`]. use actix_http::uri::{PathAndQuery, Uri}; use actix_service::{Service, Transform}; use actix_utils::future::{ready, Ready}; use bytes::Bytes; #[cfg(feature = "unicode")] use regex::Regex; #[cfg(not(feature = "unicode"))] use regex_lite::Regex; use crate::{ service::{ServiceRequest, ServiceResponse}, Error, }; /// Determines the behavior of the [`NormalizePath`] middleware. /// /// The default is `TrailingSlash::Trim`. #[non_exhaustive] #[derive(Debug, Clone, Copy, Default)] pub enum TrailingSlash { /// Trim trailing slashes from the end of the path. /// /// Using this will require all routes to omit trailing slashes for them to be accessible. #[default] Trim, /// Only merge any present multiple trailing slashes. /// /// This option provides the best compatibility with behavior in actix-web v2.0. MergeOnly, /// Always add a trailing slash to the end of the path. /// /// Using this will require all routes have a trailing slash for them to be accessible. Always, } /// Middleware for normalizing a request's path so that routes can be matched more flexibly. /// /// # Normalization Steps /// - Merges consecutive slashes into one. (For example, `/path//one` always becomes `/path/one`.) /// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing /// slashes as-is, depending on which [`TrailingSlash`] variant is supplied /// to [`new`](NormalizePath::new()). /// /// # Default Behavior /// The default constructor chooses to strip trailing slashes from the end of paths with them /// ([`TrailingSlash::Trim`]). The implication is that route definitions should be defined without /// trailing slashes or else they will be inaccessible (or vice versa when using the /// `TrailingSlash::Always` behavior), as shown in the example tests below. /// /// # Examples /// ``` /// use actix_web::{web, middleware, App}; /// /// # actix_web::rt::System::new().block_on(async { /// let app = App::new() /// .wrap(middleware::NormalizePath::trim()) /// .route("/test", web::get().to(|| async { "test" })) /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" })); /// /// use actix_web::http::StatusCode; /// use actix_web::test::{call_service, init_service, TestRequest}; /// /// let app = init_service(app).await; /// /// let req = TestRequest::with_uri("/test").to_request(); /// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::OK); /// /// let req = TestRequest::with_uri("/test/").to_request(); /// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::OK); /// /// let req = TestRequest::with_uri("/unmatchable").to_request(); /// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// /// let req = TestRequest::with_uri("/unmatchable/").to_request(); /// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// # }) /// ``` #[derive(Debug, Clone, Copy)] pub struct NormalizePath(TrailingSlash); impl Default for NormalizePath { fn default() -> Self { log::warn!( "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \ in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`." ); Self(TrailingSlash::Trim) } } impl NormalizePath { /// Create new `NormalizePath` middleware with the specified trailing slash style. pub fn new(trailing_slash_style: TrailingSlash) -> Self { Self(trailing_slash_style) } /// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics. /// /// Use this instead of `NormalizePath::default()` to avoid deprecation warning. pub fn trim() -> Self { Self::new(TrailingSlash::Trim) } } impl Transform for NormalizePath where S: Service, Error = Error>, S::Future: 'static, { type Response = ServiceResponse; type Error = Error; type Transform = NormalizePathNormalization; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(NormalizePathNormalization { service, merge_slash: Regex::new("//+").unwrap(), trailing_slash_behavior: self.0, })) } } pub struct NormalizePathNormalization { service: S, merge_slash: Regex, trailing_slash_behavior: TrailingSlash, } impl Service for NormalizePathNormalization where S: Service, Error = Error>, S::Future: 'static, { type Response = ServiceResponse; type Error = Error; type Future = S::Future; actix_service::forward_ready!(service); fn call(&self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); let original_path = head.uri.path(); // An empty path here means that the URI has no valid path. We skip normalization in this // case, because adding a path can make the URI invalid if !original_path.is_empty() { // Either adds a string to the end (duplicates will be removed anyways) or trims all // slashes from the end let path = match self.trailing_slash_behavior { TrailingSlash::Always => format!("{}/", original_path), TrailingSlash::MergeOnly => original_path.to_string(), TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), }; // normalize multiple /'s to one / let path = self.merge_slash.replace_all(&path, "/"); // Ensure root paths are still resolvable. If resulting path is blank after previous // step it means the path was one or more slashes. Reduce to single slash. let path = if path.is_empty() { "/" } else { path.as_ref() }; // Check whether the path has been changed // // This check was previously implemented as string length comparison // // That approach fails when a trailing slash is added, // and a duplicate slash is removed, // since the length of the strings remains the same // // For example, the path "/v1//s" will be normalized to "/v1/s/" // Both of the paths have the same length, // so the change can not be deduced from the length comparison if path != original_path { let mut parts = head.uri.clone().into_parts(); let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); let path = match query { Some(q) => Bytes::from(format!("{}?{}", path, q)), None => Bytes::copy_from_slice(path.as_bytes()), }; parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap()); let uri = Uri::from_parts(parts).unwrap(); req.match_info_mut().get_mut().update(&uri); req.head_mut().uri = uri; } } self.service.call(req) } } #[cfg(test)] mod tests { use actix_http::StatusCode; use actix_service::IntoService; use super::*; use crate::{ guard::fn_guard, test::{call_service, init_service, TestRequest}, web, App, HttpResponse, }; #[actix_rt::test] async fn test_wrap() { let app = init_service( App::new() .wrap(NormalizePath::default()) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let test_uris = vec![ "/", "/?query=test", "///", "/v1//something", "/v1//something////", "//v1/something", "//v1//////something", "/v2//something?query=test", "/v2//something////?query=test", "//v2/something?query=test", "//v2//////something?query=test", ]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn trim_trailing_slashes() { let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::Trim)) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let test_uris = vec![ "/", "///", "/v1/something", "/v1/something/", "/v1/something////", "//v1//something", "//v1//something//", "/v2/something?query=test", "/v2/something/?query=test", "/v2/something////?query=test", "//v2//something?query=test", "//v2//something//?query=test", ]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn trim_root_trailing_slashes_with_query() { let app = init_service( App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( web::resource("/") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn ensure_trailing_slash() { let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::Always)) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something/").to(HttpResponse::Ok)) .service( web::resource("/v2/something/") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let test_uris = vec![ "/", "///", "/v1/something", "/v1/something/", "/v1/something////", "//v1//something", "//v1//something//", "/v2/something?query=test", "/v2/something/?query=test", "/v2/something////?query=test", "//v2//something?query=test", "//v2//something//?query=test", ]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn ensure_root_trailing_slash_with_query() { let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::Always)) .service( web::resource("/") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn keep_trailing_slash_unchanged() { let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::MergeOnly)) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service(web::resource("/v1/").to(HttpResponse::Ok)) .service( web::resource("/v2/something") .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) .await; let tests = vec![ ("/", true), // root paths should still work ("/?query=test", true), ("///", true), ("/v1/something////", false), ("/v1/something/", false), ("//v1//something", true), ("/v1/", true), ("/v1", false), ("/v1////", true), ("//v1//", true), ("///v1", false), ("/v2/something?query=test", true), ("/v2/something/?query=test", false), ("/v2/something//?query=test", false), ("//v2//something?query=test", true), ]; for (uri, success) in tests { let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri); } } #[actix_rt::test] async fn no_path() { let app = init_service( App::new() .wrap(NormalizePath::default()) .service(web::resource("/").to(HttpResponse::Ok)), ) .await; // This URI will be interpreted as an authority form, i.e. there is no path nor scheme // (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3) let req = TestRequest::with_uri("eh").to_request(); let res = call_service(&app, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { assert_eq!("/v1/something", req.path()); ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); let test_uris = vec![ "/v1//something////", "///v1/something", "//v1///something", "/v1//something", ]; for uri in test_uris { let req = TestRequest::with_uri(uri).to_srv_request(); let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success(), "Failed uri: {}", uri); } } #[actix_rt::test] async fn should_normalize_nothing() { const URI: &str = "/v1/something"; let srv = |req: ServiceRequest| { assert_eq!(URI, req.path()); ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); let req = TestRequest::with_uri(URI).to_srv_request(); let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success()); } #[actix_rt::test] async fn should_normalize_no_trail() { let srv = |req: ServiceRequest| { assert_eq!("/v1/something", req.path()); ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); let req = TestRequest::with_uri("/v1/something/").to_srv_request(); let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success()); } } actix-web-4.9.0/src/redirect.rs000064400000000000000000000201571046102023000144640ustar 00000000000000//! See [`Redirect`] for service/responder documentation. use std::borrow::Cow; use actix_utils::future::ready; use crate::{ dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest}, http::{header::LOCATION, StatusCode}, HttpRequest, HttpResponse, Responder, }; /// An HTTP service for redirecting one path to another path or URL. /// /// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN /// article][mdn-redirects] on why 307 is preferred over 302. /// /// # Examples /// As service: /// ``` /// use actix_web::{web, App}; /// /// App::new() /// // redirect "/duck" to DuckDuckGo /// .service(web::redirect("/duck", "https://duck.com")) /// .service( /// // redirect "/api/old" to "/api/new" /// web::scope("/api").service(web::redirect("/old", "/new")) /// ); /// ``` /// /// As responder: /// ``` /// use actix_web::{web::Redirect, Responder}; /// /// async fn handler() -> impl Responder { /// // sends a permanent (308) redirect to duck.com /// Redirect::to("https://duck.com").permanent() /// } /// # actix_web::web::to(handler); /// ``` /// /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections #[derive(Debug, Clone)] pub struct Redirect { from: Cow<'static, str>, to: Cow<'static, str>, status_code: StatusCode, } impl Redirect { /// Construct a new `Redirect` service that matches a path. /// /// This service will match exact paths equal to `from` within the current scope. I.e., when /// registered on the root `App`, it will match exact, whole paths. But when registered on a /// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like /// a normal `Resource` or `Route`. /// /// The `to` argument can be path or URL; whatever is provided shall be used verbatim when /// setting the redirect location. This means that relative paths can be used to navigate /// relatively to matched paths. /// /// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has /// no meaning in that context. /// /// # Examples /// ``` /// # use actix_web::{web::Redirect, App}; /// App::new() /// // redirects "/oh/hi/mark" to "/oh/bye/johnny" /// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny")); /// ``` pub fn new(from: impl Into>, to: impl Into>) -> Self { Self { from: from.into(), to: to.into(), status_code: StatusCode::TEMPORARY_REDIRECT, } } /// Construct a new `Redirect` to use as a responder. /// /// Only receives the `to` argument since responders do not need to do route matching. /// /// # Examples /// ``` /// use actix_web::{web::Redirect, Responder}; /// /// async fn admin_page() -> impl Responder { /// // sends a temporary 307 redirect to the login path /// Redirect::to("/login") /// } /// # actix_web::web::to(admin_page); /// ``` pub fn to(to: impl Into>) -> Self { Self { from: "/".into(), to: to.into(), status_code: StatusCode::TEMPORARY_REDIRECT, } } /// Use the "308 Permanent Redirect" status when responding. /// /// See [this MDN article][mdn-redirects] on why 308 is preferred over 301. /// /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections pub fn permanent(self) -> Self { self.using_status_code(StatusCode::PERMANENT_REDIRECT) } /// Use the "307 Temporary Redirect" status when responding. /// /// See [this MDN article][mdn-redirects] on why 307 is preferred over 302. /// /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections pub fn temporary(self) -> Self { self.using_status_code(StatusCode::TEMPORARY_REDIRECT) } /// Use the "303 See Other" status when responding. /// /// This status code is semantically correct as the response to a successful login, for example. pub fn see_other(self) -> Self { self.using_status_code(StatusCode::SEE_OTHER) } /// Allows the use of custom status codes for less common redirect types. /// /// In most cases, the default status ("308 Permanent Redirect") or using the `temporary` /// method, which uses the "307 Temporary Redirect" status have more consistent behavior than /// 301 and 302 codes, respectively. /// /// ``` /// # use actix_web::{http::StatusCode, web::Redirect}; /// // redirects would use "301 Moved Permanently" status code /// Redirect::new("/old", "/new") /// .using_status_code(StatusCode::MOVED_PERMANENTLY); /// /// // redirects would use "302 Found" status code /// Redirect::new("/old", "/new") /// .using_status_code(StatusCode::FOUND); /// ``` pub fn using_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; self } } impl HttpServiceFactory for Redirect { fn register(self, config: &mut AppService) { let redirect = self.clone(); let rdef = ResourceDef::new(self.from.into_owned()); let redirect_factory = fn_service(move |mut req: ServiceRequest| { let res = redirect.clone().respond_to(req.parts_mut().0); ready(Ok(req.into_response(res.map_into_boxed_body()))) }); config.register_service(rdef, None, redirect_factory, None) } } impl Responder for Redirect { type Body = (); fn respond_to(self, _req: &HttpRequest) -> HttpResponse { let mut res = HttpResponse::with_body(self.status_code, ()); if let Ok(hdr_val) = self.to.parse() { res.headers_mut().insert(LOCATION, hdr_val); } else { log::error!( "redirect target location can not be converted to header value: {:?}", self.to, ); } res } } #[cfg(test)] mod tests { use super::*; use crate::{dev::Service, test, App}; #[actix_rt::test] async fn absolute_redirects() { let redirector = Redirect::new("/one", "/two").permanent(); let svc = test::init_service(App::new().service(redirector)).await; let req = test::TestRequest::default().uri("/one").to_request(); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::from_u16(308).unwrap()); let hdr = res.headers().get(&LOCATION).unwrap(); assert_eq!(hdr.to_str().unwrap(), "/two"); } #[actix_rt::test] async fn relative_redirects() { let redirector = Redirect::new("/one", "two").permanent(); let svc = test::init_service(App::new().service(redirector)).await; let req = test::TestRequest::default().uri("/one").to_request(); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::from_u16(308).unwrap()); let hdr = res.headers().get(&LOCATION).unwrap(); assert_eq!(hdr.to_str().unwrap(), "two"); } #[actix_rt::test] async fn temporary_redirects() { let external_service = Redirect::new("/external", "https://duck.com"); let svc = test::init_service(App::new().service(external_service)).await; let req = test::TestRequest::default().uri("/external").to_request(); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::from_u16(307).unwrap()); let hdr = res.headers().get(&LOCATION).unwrap(); assert_eq!(hdr.to_str().unwrap(), "https://duck.com"); } #[actix_rt::test] async fn as_responder() { let responder = Redirect::to("https://duck.com"); let req = test::TestRequest::default().to_http_request(); let res = responder.respond_to(&req); assert_eq!(res.status(), StatusCode::from_u16(307).unwrap()); let hdr = res.headers().get(&LOCATION).unwrap(); assert_eq!(hdr.to_str().unwrap(), "https://duck.com"); } } actix-web-4.9.0/src/request.rs000064400000000000000000001020531046102023000143470ustar 00000000000000use std::{ cell::{Ref, RefCell, RefMut}, fmt, net, rc::Rc, str, }; use actix_http::{Message, RequestHead}; use actix_router::{Path, Url}; use actix_utils::future::{ok, Ready}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; use smallvec::SmallVec; use crate::{ app_service::AppInitServiceState, config::AppConfig, dev::{Extensions, Payload}, error::UrlGenerationError, http::{header::HeaderMap, Method, Uri, Version}, info::ConnectionInfo, rmap::ResourceMap, Error, FromRequest, HttpMessage, }; #[cfg(feature = "cookies")] struct Cookies(Vec>); /// An incoming request. #[derive(Clone)] pub struct HttpRequest { /// # Invariant /// `Rc` is used exclusively and NO `Weak` /// is allowed anywhere in the code. Weak pointer is purposely ignored when /// doing `Rc`'s ref counter check. Expect panics if this invariant is violated. pub(crate) inner: Rc, } pub(crate) struct HttpRequestInner { pub(crate) head: Message, pub(crate) path: Path, pub(crate) app_data: SmallVec<[Rc; 4]>, pub(crate) conn_data: Option>, pub(crate) extensions: Rc>, app_state: Rc, } impl HttpRequest { #[inline] pub(crate) fn new( path: Path, head: Message, app_state: Rc, app_data: Rc, conn_data: Option>, extensions: Rc>, ) -> HttpRequest { let mut data = SmallVec::<[Rc; 4]>::new(); data.push(app_data); HttpRequest { inner: Rc::new(HttpRequestInner { head, path, app_state, app_data: data, conn_data, extensions, }), } } } impl HttpRequest { /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { &self.inner.head } /// This method returns mutable reference to the request head. /// panics if multiple references of HTTP request exists. #[inline] pub(crate) fn head_mut(&mut self) -> &mut RequestHead { &mut Rc::get_mut(&mut self.inner).unwrap().head } /// Request's uri. #[inline] pub fn uri(&self) -> &Uri { &self.head().uri } /// Returns request's original full URL. /// /// Reconstructed URL is best-effort, using [`connection_info`](HttpRequest::connection_info()) /// to get forwarded scheme & host. /// /// ``` /// use actix_web::test::TestRequest; /// let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo") /// .insert_header(("host", "example.com")) /// .to_http_request(); /// /// assert_eq!( /// req.full_url().as_str(), /// "http://example.com/api?id=4&name=foo", /// ); /// ``` pub fn full_url(&self) -> url::Url { let info = self.connection_info(); let scheme = info.scheme(); let host = info.host(); let path_and_query = self .uri() .path_and_query() .map(|paq| paq.as_str()) .unwrap_or("/"); url::Url::parse(&format!("{scheme}://{host}{path_and_query}")).unwrap() } /// Read the Request method. #[inline] pub fn method(&self) -> &Method { &self.head().method } /// Read the Request Version. #[inline] pub fn version(&self) -> Version { self.head().version } #[inline] /// Returns request's headers. pub fn headers(&self) -> &HeaderMap { &self.head().headers } /// The target path of this request. #[inline] pub fn path(&self) -> &str { self.head().uri.path() } /// The query string in the URL. /// /// Example: `id=10` #[inline] pub fn query_string(&self) -> &str { self.uri().query().unwrap_or_default() } /// Returns a reference to the URL parameters container. /// /// A URL parameter is specified in the form `{identifier}`, where the identifier can be used /// later in a request handler to access the matched value for that parameter. /// /// # Percent Encoding and URL Parameters /// Because each URL parameter is able to capture multiple path segments, none of /// `["%2F", "%25", "%2B"]` found in the request URI are decoded into `["/", "%", "+"]` in order /// to preserve path integrity. If a URL parameter is expected to contain these characters, then /// it is on the user to decode them or use the [`web::Path`](crate::web::Path) extractor which /// _will_ decode these special sequences. #[inline] pub fn match_info(&self) -> &Path { &self.inner.path } /// Returns a mutable reference to the URL parameters container. /// /// # Panics /// Panics if this `HttpRequest` has been cloned. #[inline] pub(crate) fn match_info_mut(&mut self) -> &mut Path { &mut Rc::get_mut(&mut self.inner).unwrap().path } /// The resource definition pattern that matched the path. Useful for logging and metrics. /// /// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made /// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`. /// /// Returns a None when no resource is fully matched, including default services. #[inline] pub fn match_pattern(&self) -> Option { self.resource_map().match_pattern(self.path()) } /// The resource name that matched the path. Useful for logging and metrics. /// /// Returns a None when no resource is fully matched, including default services. #[inline] pub fn match_name(&self) -> Option<&str> { self.resource_map().match_name(self.path()) } /// Returns a reference a piece of connection data set in an [on-connect] callback. /// /// ```ignore /// let opt_t = req.conn_data::(); /// ``` /// /// [on-connect]: crate::HttpServer::on_connect pub fn conn_data(&self) -> Option<&T> { self.inner .conn_data .as_deref() .and_then(|container| container.get::()) } /// Generates URL for a named resource. /// /// This substitutes in sequence all URL parameters that appear in the resource itself and in /// parent [scopes](crate::web::scope), if any. /// /// It is worth noting that the characters `['/', '%']` are not escaped and therefore a single /// URL parameter may expand into multiple path segments and `elements` can be percent-encoded /// beforehand without worrying about double encoding. Any other character that is not valid in /// a URL path context is escaped using percent-encoding. /// /// # Examples /// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// fn index(req: HttpRequest) -> HttpResponse { /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate URL for "foo" resource /// HttpResponse::Ok().into() /// } /// /// let app = App::new() /// .service(web::resource("/test/{one}/{two}/{three}") /// .name("foo") // <- set resource name so it can be used in `url_for` /// .route(web::get().to(|| HttpResponse::Ok())) /// ); /// ``` pub fn url_for(&self, name: &str, elements: U) -> Result where U: IntoIterator, I: AsRef, { self.resource_map().url_for(self, name, elements) } /// Generate URL for named resource /// /// This method is similar to `HttpRequest::url_for()` but it can be used /// for urls that do not contain variable parts. pub fn url_for_static(&self, name: &str) -> Result { const NO_PARAMS: [&str; 0] = []; self.url_for(name, NO_PARAMS) } /// Get a reference to a `ResourceMap` of current application. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.app_state().rmap() } /// Returns peer socket address. /// /// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// the Actix Web server, then it would be address of this proxy. /// /// For expanded client connection information, use [`connection_info`] instead. /// /// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used. /// /// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr /// [`connection_info`]: Self::connection_info #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr } /// Returns connection info for the current request. /// /// The return type, [`ConnectionInfo`], can also be used as an extractor. /// /// # Panics /// Panics if request's extensions container is already borrowed. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { if !self.extensions().contains::() { let info = ConnectionInfo::new(self.head(), self.app_config()); self.extensions_mut().insert(info); } Ref::map(self.extensions(), |data| data.get().unwrap()) } /// Returns a reference to the application's connection configuration. #[inline] pub fn app_config(&self) -> &AppConfig { self.app_state().config() } /// Retrieves a piece of application state. /// /// Extracts any object stored with [`App::app_data()`](crate::App::app_data) (or the /// counterpart methods on [`Scope`](crate::Scope::app_data) and /// [`Resource`](crate::Resource::app_data)) during application configuration. /// /// Since the Actix Web router layers application data, the returned object will reference the /// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope` /// also stores a `u32`, and the delegated request handler falls within that `Scope`, then /// calling `.app_data::()` on an `HttpRequest` within that handler will return the /// `Scope`'s instance. However, using the same router set up and a request that does not get /// captured by the `Scope`, `.app_data::()` would return the `App`'s instance. /// /// If the state was stored using the [`Data`] wrapper, then it must also be retrieved using /// this same type. /// /// See also the [`Data`] extractor. /// /// # Examples /// ```no_run /// # use actix_web::{test::TestRequest, web::Data}; /// # let req = TestRequest::default().to_http_request(); /// # type T = u32; /// let opt_t: Option<&Data> = req.app_data::>(); /// ``` /// /// [`Data`]: crate::web::Data #[doc(alias = "state")] pub fn app_data(&self) -> Option<&T> { for container in self.inner.app_data.iter().rev() { if let Some(data) = container.get::() { return Some(data); } } None } #[inline] fn app_state(&self) -> &AppInitServiceState { &self.inner.app_state } /// Load request cookies. #[cfg(feature = "cookies")] pub fn cookies(&self) -> Result>>, CookieParseError> { use actix_http::header::COOKIE; if self.extensions().get::().is_none() { let mut cookies = Vec::new(); for hdr in self.headers().get_all(COOKIE) { let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; for cookie_str in s.split(';').map(|s| s.trim()) { if !cookie_str.is_empty() { cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); } } } self.extensions_mut().insert(Cookies(cookies)); } Ok(Ref::map(self.extensions(), |ext| { &ext.get::().unwrap().0 })) } /// Return request cookie. #[cfg(feature = "cookies")] pub fn cookie(&self, name: &str) -> Option> { if let Ok(cookies) = self.cookies() { for cookie in cookies.iter() { if cookie.name() == name { return Some(cookie.to_owned()); } } } None } } impl HttpMessage for HttpRequest { type Stream = (); #[inline] fn headers(&self) -> &HeaderMap { &self.head().headers } #[inline] fn extensions(&self) -> Ref<'_, Extensions> { self.inner.extensions.borrow() } #[inline] fn extensions_mut(&self) -> RefMut<'_, Extensions> { self.inner.extensions.borrow_mut() } #[inline] fn take_payload(&mut self) -> Payload { Payload::None } } impl Drop for HttpRequest { fn drop(&mut self) { // if possible, contribute to current worker's HttpRequest allocation pool // This relies on no weak references to inner existing anywhere within the codebase. if let Some(inner) = Rc::get_mut(&mut self.inner) { if inner.app_state.pool().is_available() { // clear additional app_data and keep the root one for reuse. inner.app_data.truncate(1); // Inner is borrowed mut here and; get req data mutably to reduce borrow check. Also // we know the req_data Rc will not have any clones at this point to unwrap is okay. Rc::get_mut(&mut inner.extensions) .unwrap() .get_mut() .clear(); // We can't use the same trick as req data because the conn_data is held by the // dispatcher, too. inner.conn_data = None; // a re-borrow of pool is necessary here. let req = Rc::clone(&self.inner); self.app_state().pool().push(req); } } } } /// It is possible to get `HttpRequest` as an extractor handler parameter /// /// # Examples /// ``` /// use actix_web::{web, App, HttpRequest}; /// use serde::Deserialize; /// /// /// extract `Thing` from request /// async fn index(req: HttpRequest) -> String { /// format!("Got thing: {:?}", req) /// } /// /// let app = App::new().service( /// web::resource("/users/{first}").route( /// web::get().to(index)) /// ); /// ``` impl FromRequest for HttpRequest { type Error = Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.clone()) } } impl fmt::Debug for HttpRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( f, "\nHttpRequest {:?} {}:{}", self.inner.head.version, self.inner.head.method, self.path() )?; if !self.query_string().is_empty() { writeln!(f, " query: ?{:?}", self.query_string())?; } if !self.match_info().is_empty() { writeln!(f, " params: {:?}", self.match_info())?; } writeln!(f, " headers:")?; for (key, val) in self.headers().iter() { match key { // redact sensitive header values from debug output &crate::http::header::AUTHORIZATION | &crate::http::header::PROXY_AUTHORIZATION | &crate::http::header::COOKIE => writeln!(f, " {:?}: {:?}", key, "*redacted*")?, _ => writeln!(f, " {:?}: {:?}", key, val)?, } } Ok(()) } } /// Slab-allocated `HttpRequest` Pool /// /// Since request processing may yield for asynchronous events to complete, a worker may have many /// requests in-flight at any time. Pooling requests like this amortizes the performance and memory /// costs of allocating and de-allocating HttpRequest objects as frequently as they otherwise would. /// /// Request objects are added when they are dropped (see `::drop`) and re-used /// in `::call` when there are available objects in the list. /// /// The pool's default capacity is 128 items. pub(crate) struct HttpRequestPool { inner: RefCell>>, cap: usize, } impl Default for HttpRequestPool { fn default() -> Self { Self::with_capacity(128) } } impl HttpRequestPool { pub(crate) fn with_capacity(cap: usize) -> Self { HttpRequestPool { inner: RefCell::new(Vec::with_capacity(cap)), cap, } } /// Re-use a previously allocated (but now completed/discarded) HttpRequest object. #[inline] pub(crate) fn pop(&self) -> Option { self.inner .borrow_mut() .pop() .map(|inner| HttpRequest { inner }) } /// Check if the pool still has capacity for request storage. #[inline] pub(crate) fn is_available(&self) -> bool { self.inner.borrow_mut().len() < self.cap } /// Push a request to pool. #[inline] pub(crate) fn push(&self, req: Rc) { self.inner.borrow_mut().push(req); } /// Clears all allocated HttpRequest objects. pub(crate) fn clear(&self) { self.inner.borrow_mut().clear() } } #[cfg(test)] mod tests { use bytes::Bytes; use super::*; use crate::{ dev::{ResourceDef, Service}, http::{header, StatusCode}, test::{self, call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, }; #[test] fn test_debug() { let req = TestRequest::default() .insert_header(("content-type", "text/plain")) .to_http_request(); let dbg = format!("{:?}", req); assert!(dbg.contains("HttpRequest")); } #[test] #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .append_header((header::COOKIE, "cookie1=value1")) .append_header((header::COOKIE, "cookie2=value2")) .to_http_request(); { let cookies = req.cookies().unwrap(); assert_eq!(cookies.len(), 2); assert_eq!(cookies[0].name(), "cookie1"); assert_eq!(cookies[0].value(), "value1"); assert_eq!(cookies[1].name(), "cookie2"); assert_eq!(cookies[1].value(), "value2"); } let cookie = req.cookie("cookie1"); assert!(cookie.is_some()); let cookie = cookie.unwrap(); assert_eq!(cookie.name(), "cookie1"); assert_eq!(cookie.value(), "value1"); let cookie = req.cookie("cookie-unknown"); assert!(cookie.is_none()); } #[test] fn test_request_query() { let req = TestRequest::with_uri("/?id=test").to_http_request(); assert_eq!(req.query_string(), "id=test"); } #[test] fn test_url_for() { let mut res = ResourceDef::new("/user/{name}.{ext}"); res.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut res, None); assert!(rmap.has_resource("/user/test.html")); assert!(!rmap.has_resource("/test/unknown")); let req = TestRequest::default() .insert_header((header::HOST, "www.rust-lang.org")) .rmap(rmap) .to_http_request(); assert_eq!( req.url_for("unknown", ["test"]), Err(UrlGenerationError::ResourceNotFound) ); assert_eq!( req.url_for("index", ["test"]), Err(UrlGenerationError::NotEnoughElements) ); let url = req.url_for("index", ["test", "html"]); assert_eq!( url.ok().unwrap().as_str(), "http://www.rust-lang.org/user/test.html" ); } #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); let req = TestRequest::with_uri("/test") .insert_header((header::HOST, "www.rust-lang.org")) .rmap(rmap) .to_http_request(); let url = req.url_for_static("index"); assert_eq!( url.ok().unwrap().as_str(), "http://www.rust-lang.org/index.html" ); } #[test] fn test_match_name() { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); let req = TestRequest::default() .uri("/index.html") .rmap(rmap) .to_http_request(); assert_eq!(req.match_name(), Some("index")); } #[test] fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); rdef.set_name("youtube"); let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); let req = TestRequest::default().rmap(rmap).to_http_request(); let url = req.url_for("youtube", ["oHg5SJYRHA0"]); assert_eq!( url.ok().unwrap().as_str(), "https://youtube.com/watch/oHg5SJYRHA0" ); } #[actix_rt::test] async fn test_drop_http_request_pool() { let srv = init_service( App::new().service(web::resource("/").to(|req: HttpRequest| { HttpResponse::Ok() .insert_header(("pool_cap", req.app_state().pool().cap)) .finish() })), ) .await; let req = TestRequest::default().to_request(); let resp = call_service(&srv, req).await; drop(srv); assert_eq!(resp.headers().get("pool_cap").unwrap(), "128"); } #[actix_rt::test] async fn test_data() { let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } }, ))) .await; let req = TestRequest::default().to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let srv = init_service(App::new().app_data(10u32).service(web::resource("/").to( |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } }, ))) .await; let req = TestRequest::default().to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_cascading_data() { #[allow(dead_code)] fn echo_usize(req: HttpRequest) -> HttpResponse { let num = req.app_data::().unwrap(); HttpResponse::Ok().body(num.to_string()) } let srv = init_service( App::new() .app_data(88usize) .service(web::resource("/").route(web::get().to(echo_usize))) .service( web::resource("/one") .app_data(1u32) .route(web::get().to(echo_usize)), ), ) .await; let req = TestRequest::get().uri("/").to_request(); let resp = srv.call(req).await.unwrap(); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"88")); let req = TestRequest::get().uri("/one").to_request(); let resp = srv.call(req).await.unwrap(); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"88")); } #[actix_rt::test] async fn test_overwrite_data() { #[allow(dead_code)] fn echo_usize(req: HttpRequest) -> HttpResponse { let num = req.app_data::().unwrap(); HttpResponse::Ok().body(num.to_string()) } let srv = init_service( App::new() .app_data(88usize) .service(web::resource("/").route(web::get().to(echo_usize))) .service( web::resource("/one") .app_data(1usize) .route(web::get().to(echo_usize)), ), ) .await; let req = TestRequest::get().uri("/").to_request(); let resp = srv.call(req).await.unwrap(); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"88")); let req = TestRequest::get().uri("/one").to_request(); let resp = srv.call(req).await.unwrap(); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"1")); } #[actix_rt::test] async fn test_app_data_dropped() { struct Tracker { pub dropped: bool, } struct Foo { tracker: Rc>, } impl Drop for Foo { fn drop(&mut self) { self.tracker.borrow_mut().dropped = true; } } let tracker = Rc::new(RefCell::new(Tracker { dropped: false })); { let tracker2 = Rc::clone(&tracker); let srv = init_service(App::new().service(web::resource("/").to( move |req: HttpRequest| { req.extensions_mut().insert(Foo { tracker: Rc::clone(&tracker2), }); HttpResponse::Ok() }, ))) .await; let req = TestRequest::default().to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } assert!(tracker.borrow().dropped); } #[actix_rt::test] async fn extract_path_pattern() { let srv = init_service( App::new().service( web::scope("/user/{id}") .service(web::resource("/profile").route(web::get().to( move |req: HttpRequest| { assert_eq!(req.match_pattern(), Some("/user/{id}/profile".to_owned())); HttpResponse::Ok().finish() }, ))) .default_service(web::to(move |req: HttpRequest| { assert!(req.match_pattern().is_none()); HttpResponse::Ok().finish() })), ), ) .await; let req = TestRequest::get().uri("/user/22/profile").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/user/22/not-exist").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] async fn extract_path_pattern_complex() { let srv = init_service( App::new() .service(web::scope("/user").service(web::scope("/{id}").service( web::resource("").to(move |req: HttpRequest| { assert_eq!(req.match_pattern(), Some("/user/{id}".to_owned())); HttpResponse::Ok().finish() }), ))) .service(web::resource("/").to(move |req: HttpRequest| { assert_eq!(req.match_pattern(), Some("/".to_owned())); HttpResponse::Ok().finish() })) .default_service(web::to(move |req: HttpRequest| { assert!(req.match_pattern().is_none()); HttpResponse::Ok().finish() })), ) .await; let req = TestRequest::get().uri("/user/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/not-exist").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] async fn url_for_closest_named_resource() { // we mount the route named 'nested' on 2 different scopes, 'a' and 'b' let srv = test::init_service( App::new() .service( web::scope("/foo") .service(web::resource("/nested").name("nested").route(web::get().to( |req: HttpRequest| { HttpResponse::Ok() .body(format!("{}", req.url_for_static("nested").unwrap())) }, ))) .service(web::scope("/baz").service(web::resource("deep"))) .service(web::resource("{foo_param}")), ) .service(web::scope("/bar").service( web::resource("/nested").name("nested").route(web::get().to( |req: HttpRequest| { HttpResponse::Ok() .body(format!("{}", req.url_for_static("nested").unwrap())) }, )), )), ) .await; let foo_resp = test::call_service(&srv, TestRequest::with_uri("/foo/nested").to_request()).await; assert_eq!(foo_resp.status(), StatusCode::OK); let body = read_body(foo_resp).await; // `body` equals http://localhost:8080/bar/nested // because nested from /bar overrides /foo's // to do this any other way would require something like a custom tree search // see https://github.com/actix/actix-web/issues/1763 assert_eq!(body, "http://localhost:8080/bar/nested"); let bar_resp = test::call_service(&srv, TestRequest::with_uri("/bar/nested").to_request()).await; assert_eq!(bar_resp.status(), StatusCode::OK); let body = read_body(bar_resp).await; assert_eq!(body, "http://localhost:8080/bar/nested"); } #[test] fn authorization_header_hidden_in_debug() { let authorization_header = "Basic bXkgdXNlcm5hbWU6bXkgcGFzc3dvcmQK"; let req = TestRequest::get() .insert_header((crate::http::header::AUTHORIZATION, authorization_header)) .to_http_request(); assert!(!format!("{:?}", req).contains(authorization_header)); } #[test] fn proxy_authorization_header_hidden_in_debug() { let proxy_authorization_header = "secret value"; let req = TestRequest::get() .insert_header(( crate::http::header::PROXY_AUTHORIZATION, proxy_authorization_header, )) .to_http_request(); assert!(!format!("{:?}", req).contains(proxy_authorization_header)); } #[test] fn cookie_header_hidden_in_debug() { let cookie_header = "secret"; let req = TestRequest::get() .insert_header((crate::http::header::COOKIE, cookie_header)) .to_http_request(); assert!(!format!("{:?}", req).contains(cookie_header)); } #[test] fn other_header_visible_in_debug() { let location_header = "192.0.0.1"; let req = TestRequest::get() .insert_header((crate::http::header::LOCATION, location_header)) .to_http_request(); assert!(format!("{:?}", req).contains(location_header)); } #[test] fn check_full_url() { let req = TestRequest::with_uri("/api?id=4&name=foo").to_http_request(); assert_eq!( req.full_url().as_str(), "http://localhost:8080/api?id=4&name=foo", ); let req = TestRequest::with_uri("https://example.com/api?id=4&name=foo").to_http_request(); assert_eq!( req.full_url().as_str(), "https://example.com/api?id=4&name=foo", ); let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo") .insert_header(("host", "example.com")) .to_http_request(); assert_eq!( req.full_url().as_str(), "http://example.com/api?id=4&name=foo", ); } } actix-web-4.9.0/src/request_data.rs000064400000000000000000000133421046102023000153420ustar 00000000000000use std::{any::type_name, ops::Deref}; use actix_utils::future::{err, ok, Ready}; use crate::{ dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpMessage as _, HttpRequest, }; /// Request-local data extractor. /// /// Request-local data is arbitrary data attached to an individual request, usually /// by middleware. It can be set via `extensions_mut` on [`HttpRequest`][htr_ext_mut] /// or [`ServiceRequest`][srv_ext_mut]. /// /// Unlike app data, request data is dropped when the request has finished processing. This makes it /// useful as a kind of messaging system between middleware and request handlers. It uses the same /// types-as-keys storage system as app data. /// /// # Mutating Request Data /// Note that since extractors must output owned data, only types that `impl Clone` can use this /// extractor. A clone is taken of the required request data and can, therefore, not be directly /// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or /// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not /// provided to make this potential foot-gun more obvious. /// /// # Examples /// ```no_run /// # use actix_web::{web, HttpResponse, HttpRequest, Responder, HttpMessage as _}; /// #[derive(Debug, Clone, PartialEq)] /// struct FlagFromMiddleware(String); /// /// /// Use the `ReqData` extractor to access request data in a handler. /// async fn handler( /// req: HttpRequest, /// opt_flag: Option>, /// ) -> impl Responder { /// // use an option extractor if middleware is not guaranteed to add this type of req data /// if let Some(flag) = opt_flag { /// assert_eq!(&flag.into_inner(), req.extensions().get::().unwrap()); /// } /// /// HttpResponse::Ok() /// } /// ``` /// /// [htr_ext_mut]: crate::HttpRequest::extensions_mut /// [srv_ext_mut]: crate::dev::ServiceRequest::extensions_mut #[derive(Debug, Clone)] pub struct ReqData(T); impl ReqData { /// Consumes the `ReqData`, returning its wrapped data. pub fn into_inner(self) -> T { self.0 } } impl Deref for ReqData { type Target = T; fn deref(&self) -> &T { &self.0 } } impl FromRequest for ReqData { type Error = Error; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { if let Some(st) = req.extensions().get::() { ok(ReqData(st.clone())) } else { log::debug!( "Failed to construct App-level ReqData extractor. \ Request path: {:?} (type: {})", req.path(), type_name::(), ); err(ErrorInternalServerError( "Missing expected request extension data", )) } } } #[cfg(test)] mod tests { use std::{cell::RefCell, rc::Rc}; use futures_util::TryFutureExt as _; use super::*; use crate::{ dev::Service, http::{Method, StatusCode}, test::{init_service, TestRequest}, web, App, HttpMessage, HttpResponse, }; #[actix_rt::test] async fn req_data_extractor() { let srv = init_service( App::new() .wrap_fn(|req, srv| { if req.method() == Method::POST { req.extensions_mut().insert(42u32); } srv.call(req) }) .service(web::resource("/test").to( |req: HttpRequest, data: Option>| { if req.method() != Method::POST { assert!(data.is_none()); } if let Some(data) = data { assert_eq!(*data, 42); assert_eq!( Some(data.into_inner()), req.extensions().get::().copied() ); } HttpResponse::Ok() }, )), ) .await; let req = TestRequest::get().uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::post().uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn req_data_internal_mutability() { let srv = init_service( App::new() .wrap_fn(|req, srv| { let data_before = Rc::new(RefCell::new(42u32)); req.extensions_mut().insert(data_before); srv.call(req).map_ok(|res| { { let ext = res.request().extensions(); let data_after = ext.get::>>().unwrap(); assert_eq!(*data_after.borrow(), 53u32); } res }) }) .default_service(web::to(|data: ReqData>>| { assert_eq!(*data.borrow(), 42); *data.borrow_mut() += 11; assert_eq!(*data.borrow(), 53); HttpResponse::Ok() })), ) .await; let req = TestRequest::get().uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } } actix-web-4.9.0/src/resource.rs000064400000000000000000000672451046102023000145230ustar 00000000000000use std::{cell::RefCell, fmt, future::Future, rc::Rc}; use actix_http::Extensions; use actix_router::{IntoPatterns, Patterns}; use actix_service::{ apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ body::MessageBody, data::Data, dev::{ensure_leading_slash, AppService, ResourceDef}, guard::{self, Guard}, handler::Handler, http::header, route::{Route, RouteService}, service::{ BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, ServiceResponse, }, web, Error, FromRequest, HttpResponse, Responder, }; /// A collection of [`Route`]s that respond to the same path pattern. /// /// Resource in turn has at least one route. Route consists of an handlers objects and list of /// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern /// for configuration. During request handling, resource object iterate through all routes and check /// guards for specific route, if request matches all guards, route considered matched and route /// handler get called. /// /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( /// web::resource("/") /// .get(|| HttpResponse::Ok()) /// .post(|| async { "Hello World!" }) /// ); /// ``` /// /// If no matching route is found, an empty 405 response is returned which includes an /// [appropriate Allow header][RFC 9110 §15.5.6]. This default behavior can be overridden using /// [`default_service()`](Self::default_service). /// /// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6 pub struct Resource { endpoint: T, rdef: Patterns, name: Option, routes: Vec, app_data: Option, guards: Vec>, default: BoxedHttpServiceFactory, factory_ref: Rc>>, } impl Resource { /// Constructs new resource that matches a `path` pattern. pub fn new(path: T) -> Resource { let factory_ref = Rc::new(RefCell::new(None)); Resource { routes: Vec::new(), rdef: path.patterns(), name: None, endpoint: ResourceEndpoint::new(Rc::clone(&factory_ref)), factory_ref, guards: Vec::new(), app_data: None, default: boxed::factory(fn_service(|req: ServiceRequest| async { use crate::HttpMessage as _; let allowed = req.extensions().get::().cloned(); if let Some(methods) = allowed { Ok(req.into_response( HttpResponse::MethodNotAllowed() .insert_header(header::Allow(methods.0)) .finish(), )) } else { Ok(req.into_response(HttpResponse::MethodNotAllowed())) } })), } } } impl Resource where T: ServiceFactory, { /// Set resource name. /// /// Name is used for url generation. pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self } /// Add match guard to a resource. /// /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// /// let app = App::new() /// .service( /// web::resource("/app") /// .guard(guard::Header("content-type", "text/plain")) /// .route(web::get().to(index)) /// ) /// .service( /// web::resource("/app") /// .guard(guard::Header("content-type", "text/json")) /// .route(web::get().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); self } pub(crate) fn add_guards(mut self, guards: Vec>) -> Self { self.guards.extend(guards); self } /// Register a new route. /// /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// let app = App::new().service( /// web::resource("/").route( /// web::route() /// .guard(guard::Any(guard::Get()).or(guard::Put())) /// .guard(guard::Header("Content-Type", "text/plain")) /// .to(|| HttpResponse::Ok())) /// ); /// ``` /// /// Multiple routes could be added to a resource. Resource object uses /// match guards for route selection. /// /// ``` /// use actix_web::{web, guard, App}; /// /// let app = App::new().service( /// web::resource("/container/") /// .route(web::get().to(get_handler)) /// .route(web::post().to(post_handler)) /// .route(web::delete().to(delete_handler)) /// ); /// /// # async fn get_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } /// # async fn post_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } /// # async fn delete_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } /// ``` pub fn route(mut self, route: Route) -> Self { self.routes.push(route); self } /// Add resource data. /// /// Data of different types from parent contexts will still be accessible. Any `Data` types /// set here can be extracted in handlers using the `Data` extractor. /// /// # Examples /// ``` /// use std::cell::Cell; /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { /// count: std::cell::Cell, /// } /// /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { /// // note this cannot use the Data extractor because it was not added with it /// let incr = *req.app_data::().unwrap(); /// assert_eq!(incr, 3); /// /// // update counter using other value from app data /// counter.count.set(counter.count.get() + incr); /// /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// /// let app = App::new().service( /// web::resource("/") /// .app_data(3usize) /// .app_data(web::Data::new(MyData { count: Default::default() })) /// .route(web::get().to(handler)) /// ); /// ``` #[doc(alias = "manage")] pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) .insert(data); self } /// Add resource data after wrapping in `Data`. /// /// Deprecated in favor of [`app_data`](Self::app_data). #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } /// Register a new route and add handler. This route matches all requests. /// /// ``` /// use actix_web::{App, HttpRequest, HttpResponse, web}; /// /// async fn index(req: HttpRequest) -> HttpResponse { /// todo!() /// } /// /// App::new().service(web::resource("/").to(index)); /// ``` /// /// This is shortcut for: /// /// ``` /// # use actix_web::*; /// # async fn index(req: HttpRequest) -> HttpResponse { todo!() } /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` pub fn to(mut self, handler: F) -> Self where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, { self.routes.push(Route::new().to(handler)); self } /// Registers a resource middleware. /// /// `mw` is a middleware component (type), that can modify the request and response across all /// routes managed by this `Resource`. /// /// See [`App::wrap`](crate::App::wrap) for more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap( self, mw: M, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where M: Transform< T::Service, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody, { Resource { endpoint: apply(mw, self.endpoint), rdef: self.rdef, name: self.name, guards: self.guards, routes: self.routes, default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, } } /// Registers a resource function middleware. /// /// `mw` is a closure that runs during inbound and/or outbound processing in the request /// life-cycle (request -> response), modifying request/response as necessary, across all /// requests handled by the `Resource`. /// /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap_fn( self, mw: F, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, R: Future, Error>>, B: MessageBody, { Resource { endpoint: apply_fn_factory(self.endpoint, mw), rdef: self.rdef, name: self.name, guards: self.guards, routes: self.routes, default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, } } /// Sets the default service to be used if no matching route is found. /// /// Unlike [`Scope`]s, a `Resource` does _not_ inherit its parent's default service. You can /// use a [`Route`] as default service. /// /// If a custom default service is not registered, an empty `405 Method Not Allowed` response /// with an appropriate Allow header will be sent instead. /// /// # Examples /// ``` /// use actix_web::{App, HttpResponse, web}; /// /// let resource = web::resource("/test") /// .route(web::get().to(HttpResponse::Ok)) /// .default_service(web::to(|| { /// HttpResponse::BadRequest() /// })); /// /// App::new().service(resource); /// ``` /// /// [`Scope`]: crate::Scope pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, U: ServiceFactory + 'static, U::InitError: fmt::Debug, { // create and configure default resource self.default = boxed::factory( f.into_factory() .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)), ); self } } macro_rules! route_shortcut { ($method_fn:ident, $method_upper:literal) => { #[doc = concat!(" Adds a ", $method_upper, " route.")] /// /// Use [`route`](Self::route) if you need to add additional guards. /// /// # Examples /// /// ``` /// # use actix_web::web; /// web::resource("/") #[doc = concat!(" .", stringify!($method_fn), "(|| async { \"Hello World!\" })")] /// # ; /// ``` pub fn $method_fn(self, handler: F) -> Self where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, { self.route(web::$method_fn().to(handler)) } }; } /// Concise routes for well-known HTTP methods. impl Resource where T: ServiceFactory, { route_shortcut!(get, "GET"); route_shortcut!(post, "POST"); route_shortcut!(put, "PUT"); route_shortcut!(patch, "PATCH"); route_shortcut!(delete, "DELETE"); route_shortcut!(head, "HEAD"); route_shortcut!(trace, "TRACE"); } impl HttpServiceFactory for Resource where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { None } else { Some(std::mem::take(&mut self.guards)) }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(ensure_leading_slash(self.rdef.clone())) } else { ResourceDef::new(self.rdef.clone()) }; if let Some(ref name) = self.name { rdef.set_name(name); } *self.factory_ref.borrow_mut() = Some(ResourceFactory { routes: self.routes, default: self.default, }); let resource_data = self.app_data.map(Rc::new); // wraps endpoint service (including middleware) call and injects app data for this scope let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { if let Some(ref data) = resource_data { req.add_data_container(Rc::clone(data)); } let fut = srv.call(req); async { Ok(fut.await?.map_into_boxed_body()) } }); config.register_service(rdef, guards, endpoint, None) } } pub struct ResourceFactory { routes: Vec, default: BoxedHttpServiceFactory, } impl ServiceFactory for ResourceFactory { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = ResourceService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { // construct default service factory future. let default_fut = self.default.new_service(()); // construct route service factory futures let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(()))); Box::pin(async move { let default = default_fut.await?; let routes = factory_fut .await .into_iter() .collect::, _>>()?; Ok(ResourceService { routes, default }) }) } } pub struct ResourceService { routes: Vec, default: BoxedHttpService, } impl Service for ResourceService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { for route in &self.routes { if route.check(&mut req) { return route.call(req); } } self.default.call(req) } } #[doc(hidden)] pub struct ResourceEndpoint { factory: Rc>>, } impl ResourceEndpoint { fn new(factory: Rc>>) -> Self { ResourceEndpoint { factory } } } impl ServiceFactory for ResourceEndpoint { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = ResourceService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { self.factory.borrow().as_ref().unwrap().new_service(()) } } #[cfg(test)] mod tests { use std::time::Duration; use actix_rt::time::sleep; use actix_utils::future::ok; use super::*; use crate::{ http::{header::HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, test::{call_service, init_service, TestRequest}, App, HttpMessage, }; #[test] fn can_be_returned_from_fn() { fn my_resource_1() -> Resource { web::resource("/test1").route(web::get().to(|| async { "hello" })) } fn my_resource_2() -> Resource< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > { web::resource("/test2") .wrap_fn(|req, srv| { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) .route(web::get().to(|| async { "hello" })) } fn my_resource_3() -> impl HttpServiceFactory { web::resource("/test3").route(web::get().to(|| async { "hello" })) } App::new() .service(my_resource_1()) .service(my_resource_2()) .service(my_resource_3()); } #[actix_rt::test] async fn test_middleware() { let srv = init_service( App::new().service( web::resource("/test") .name("test") .wrap( DefaultHeaders::new() .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .route(web::get().to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_middleware_fn() { let srv = init_service( App::new().service( web::resource("/test") .wrap_fn(|req, srv| { let fut = srv.call(req); async { fut.await.map(|mut res| { res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); res }) } }) .route(web::get().to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_to() { let srv = init_service(App::new().service(web::resource("/test").to(|| async { sleep(Duration::from_millis(100)).await; Ok::<_, Error>(HttpResponse::Ok()) }))) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_pattern() { let srv = init_service(App::new().service( web::resource(["/test", "/test2"]).to(|| async { Ok::<_, Error>(HttpResponse::Ok()) }), )) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test2").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_default_resource() { let srv = init_service( App::new() .service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .route(web::delete().to(HttpResponse::Ok)), ) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!( resp.headers().get(header::ALLOW).unwrap().as_bytes(), b"GET, DELETE" ); let srv = init_service( App::new().service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_resource_guards() { let srv = init_service( App::new() .service( web::resource("/test/{p}") .guard(guard::Get()) .to(HttpResponse::Ok), ) .service( web::resource("/test/{p}") .guard(guard::Put()) .to(HttpResponse::Created), ) .service( web::resource("/test/{p}") .guard(guard::Delete()) .to(HttpResponse::NoContent), ), ) .await; let req = TestRequest::with_uri("/test/it") .method(Method::GET) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test/it") .method(Method::PUT) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/test/it") .method(Method::DELETE) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NO_CONTENT); } // allow deprecated `{App, Resource}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let srv = init_service( App::new() .data(1.0f64) .data(1usize) .app_data(web::Data::new('-')) .service( web::resource("/test") .data(10usize) .app_data(web::Data::new('*')) .guard(guard::Get()) .to( |data1: web::Data, data2: web::Data, data3: web::Data| { assert_eq!(**data1, 10); assert_eq!(**data2, '*'); let error = f64::EPSILON; assert!((**data3 - 1.0).abs() < error); HttpResponse::Ok() }, ), ), ) .await; let req = TestRequest::get().uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } // allow deprecated `{App, Resource}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { let srv = init_service( App::new().data(1usize).service( web::resource("/test") .data(10usize) .default_service(web::to(|data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() })), ), ) .await; let req = TestRequest::get().uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_middleware_app_data() { let srv = init_service( App::new().service( web::resource("test") .app_data(1usize) .wrap_fn(|req, srv| { assert_eq!(req.app_data::(), Some(&1usize)); req.extensions_mut().insert(1usize); srv.call(req) }) .route(web::get().to(HttpResponse::Ok)) .default_service(|req: ServiceRequest| async move { let (req, _) = req.into_parts(); assert_eq!(req.extensions().get::(), Some(&1)); Ok(ServiceResponse::new( req, HttpResponse::BadRequest().finish(), )) }), ), ) .await; let req = TestRequest::get().uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::post().uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_middleware_body_type() { let srv = init_service( App::new().service( web::resource("/test") .wrap_fn(|req, srv| { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) .route(web::get().to(|| async { "hello" })), ), ) .await; // test if `try_into_bytes()` is preserved across scope layer use actix_http::body::MessageBody as _; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; let body = resp.into_body(); assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); } } actix-web-4.9.0/src/response/builder.rs000064400000000000000000000372631046102023000161550ustar 00000000000000use std::{ cell::{Ref, RefMut}, future::Future, pin::Pin, task::{Context, Poll}, }; use actix_http::{error::HttpError, Response, ResponseHead}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use crate::{ body::{BodyStream, BoxBody, MessageBody}, dev::Extensions, error::{Error, JsonPayloadError}, http::{ header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue}, ConnectionType, StatusCode, }, BoxError, HttpRequest, HttpResponse, Responder, }; /// An HTTP response builder. /// /// This type can be used to construct an instance of `Response` through a builder-like pattern. pub struct HttpResponseBuilder { res: Option>, error: Option, } impl HttpResponseBuilder { #[inline] /// Create response builder pub fn new(status: StatusCode) -> Self { Self { res: Some(Response::with_body(status, BoxBody::new(()))), error: None, } } /// Set HTTP status code of this response. #[inline] pub fn status(&mut self, status: StatusCode) -> &mut Self { if let Some(parts) = self.inner() { parts.status = status; } self } /// Insert a header, replacing any that were set with an equivalent field name. /// /// ``` /// use actix_web::{HttpResponse, http::header}; /// /// HttpResponse::Ok() /// .insert_header(header::ContentType(mime::APPLICATION_JSON)) /// .insert_header(("X-TEST", "value")) /// .finish(); /// ``` pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { match header.try_into_pair() { Ok((key, value)) => { parts.headers.insert(key, value); } Err(err) => self.error = Some(err.into()), }; } self } /// Append a header, keeping any that were set with an equivalent field name. /// /// ``` /// use actix_web::{HttpResponse, http::header}; /// /// HttpResponse::Ok() /// .append_header(header::ContentType(mime::APPLICATION_JSON)) /// .append_header(("X-TEST", "value1")) /// .append_header(("X-TEST", "value2")) /// .finish(); /// ``` pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { match header.try_into_pair() { Ok((key, value)) => parts.headers.append(key, value), Err(err) => self.error = Some(err.into()), }; } self } /// Replaced with [`Self::insert_header()`]. #[doc(hidden)] #[deprecated( since = "4.0.0", note = "Replaced with `insert_header((key, value))`. Will be removed in v5." )] pub fn set_header(&mut self, key: K, value: V) -> &mut Self where K: TryInto, K::Error: Into, V: TryIntoHeaderValue, { if self.error.is_some() { return self; } match (key.try_into(), value.try_into_value()) { (Ok(name), Ok(value)) => return self.insert_header((name, value)), (Err(err), _) => self.error = Some(err.into()), (_, Err(err)) => self.error = Some(err.into()), } self } /// Replaced with [`Self::append_header()`]. #[doc(hidden)] #[deprecated( since = "4.0.0", note = "Replaced with `append_header((key, value))`. Will be removed in v5." )] pub fn header(&mut self, key: K, value: V) -> &mut Self where K: TryInto, K::Error: Into, V: TryIntoHeaderValue, { if self.error.is_some() { return self; } match (key.try_into(), value.try_into_value()) { (Ok(name), Ok(value)) => return self.append_header((name, value)), (Err(err), _) => self.error = Some(err.into()), (_, Err(err)) => self.error = Some(err.into()), } self } /// Set the custom reason for the response. #[inline] pub fn reason(&mut self, reason: &'static str) -> &mut Self { if let Some(parts) = self.inner() { parts.reason = Some(reason); } self } /// Set connection type to KeepAlive #[inline] pub fn keep_alive(&mut self) -> &mut Self { if let Some(parts) = self.inner() { parts.set_connection_type(ConnectionType::KeepAlive); } self } /// Set connection type to Upgrade #[inline] pub fn upgrade(&mut self, value: V) -> &mut Self where V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { parts.set_connection_type(ConnectionType::Upgrade); } if let Ok(value) = value.try_into_value() { self.insert_header((header::UPGRADE, value)); } self } /// Force close connection, even if it is marked as keep-alive #[inline] pub fn force_close(&mut self) -> &mut Self { if let Some(parts) = self.inner() { parts.set_connection_type(ConnectionType::Close); } self } /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. #[inline] pub fn no_chunking(&mut self, len: u64) -> &mut Self { let mut buf = itoa::Buffer::new(); self.insert_header((header::CONTENT_LENGTH, buf.format(len))); if let Some(parts) = self.inner() { parts.no_chunking(true); } self } /// Set response content type. #[inline] pub fn content_type(&mut self, value: V) -> &mut Self where V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { match value.try_into_value() { Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); } Err(err) => self.error = Some(err.into()), }; } self } /// Add a cookie to the response. /// /// To send a "removal" cookie, call [`.make_removal()`](cookie::Cookie::make_removal) on the /// given cookie. See [`HttpResponse::add_removal_cookie()`] to learn more. /// /// # Examples /// Send a new cookie: /// ``` /// use actix_web::{HttpResponse, cookie::Cookie}; /// /// let res = HttpResponse::Ok() /// .cookie( /// Cookie::build("name", "value") /// .domain("www.rust-lang.org") /// .path("/") /// .secure(true) /// .http_only(true) /// .finish(), /// ) /// .finish(); /// ``` /// /// Send a removal cookie: /// ``` /// use actix_web::{HttpResponse, cookie::Cookie}; /// /// // the name, domain and path match the cookie created in the previous example /// let mut cookie = Cookie::build("name", "value-does-not-matter") /// .domain("www.rust-lang.org") /// .path("/") /// .finish(); /// cookie.make_removal(); /// /// let res = HttpResponse::Ok() /// .cookie(cookie) /// .finish(); /// ``` #[cfg(feature = "cookies")] pub fn cookie(&mut self, cookie: cookie::Cookie<'_>) -> &mut Self { match cookie.to_string().try_into_value() { Ok(hdr_val) => self.append_header((header::SET_COOKIE, hdr_val)), Err(err) => { self.error = Some(err.into()); self } } } /// Returns a reference to the response-local data/extensions container. #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { self.res .as_ref() .expect("cannot reuse response builder") .extensions() } /// Returns a mutable reference to the response-local data/extensions container. #[inline] pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { self.res .as_mut() .expect("cannot reuse response builder") .extensions_mut() } /// Set a body and build the `HttpResponse`. /// /// Unlike [`message_body`](Self::message_body), errors are converted into error /// responses immediately. /// /// `HttpResponseBuilder` can not be used after this call. pub fn body(&mut self, body: B) -> HttpResponse where B: MessageBody + 'static, { match self.message_body(body) { Ok(res) => res.map_into_boxed_body(), Err(err) => HttpResponse::from_error(err), } } /// Set a body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn message_body(&mut self, body: B) -> Result, Error> { if let Some(err) = self.error.take() { return Err(err.into()); } let res = self .res .take() .expect("cannot reuse response builder") .set_body(body); Ok(HttpResponse::from(res)) } /// Set a streaming body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn streaming(&mut self, stream: S) -> HttpResponse where S: Stream> + 'static, E: Into + 'static, { self.body(BodyStream::new(stream)) } /// Set a JSON body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn json(&mut self, value: impl Serialize) -> HttpResponse { match serde_json::to_string(&value) { Ok(body) => { let contains = if let Some(parts) = self.inner() { parts.headers.contains_key(header::CONTENT_TYPE) } else { true }; if !contains { self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); } self.body(body) } Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), } } /// Set an empty body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { self.body(()) } /// This method construct new `HttpResponseBuilder` pub fn take(&mut self) -> Self { Self { res: self.res.take(), error: self.error.take(), } } fn inner(&mut self) -> Option<&mut ResponseHead> { if self.error.is_some() { return None; } self.res.as_mut().map(Response::head_mut) } } impl From for HttpResponse { fn from(mut builder: HttpResponseBuilder) -> Self { builder.finish() } } impl From for Response { fn from(mut builder: HttpResponseBuilder) -> Self { builder.finish().into() } } impl Future for HttpResponseBuilder { type Output = Result; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { Poll::Ready(Ok(self.finish())) } } impl Responder for HttpResponseBuilder { type Body = BoxBody; #[inline] fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { self.finish() } } #[cfg(test)] mod tests { use super::*; use crate::{ body, http::header::{HeaderValue, CONTENT_TYPE}, test::assert_body_eq, }; #[test] fn test_basic_builder() { let resp = HttpResponse::Ok() .insert_header(("X-TEST", "value")) .finish(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_upgrade() { let resp = HttpResponseBuilder::new(StatusCode::OK) .upgrade("websocket") .finish(); assert!(resp.upgrade()); assert_eq!( resp.headers().get(header::UPGRADE).unwrap(), HeaderValue::from_static("websocket") ); } #[test] fn test_force_close() { let resp = HttpResponseBuilder::new(StatusCode::OK) .force_close() .finish(); assert!(!resp.keep_alive()) } #[test] fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") .body(Bytes::new()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } #[actix_rt::test] async fn test_json() { let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); assert_body_eq!(res, br#"["v1","v2","v3"]"#); let res = HttpResponse::Ok().json(["v1", "v2", "v3"]); let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); assert_body_eq!(res, br#"["v1","v2","v3"]"#); // content type override let res = HttpResponse::Ok() .insert_header((CONTENT_TYPE, "text/json")) .json(["v1", "v2", "v3"]); let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); assert_body_eq!(res, br#"["v1","v2","v3"]"#); } #[actix_rt::test] async fn test_serde_json_in_body() { let resp = HttpResponse::Ok() .body(serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap()); assert_eq!( body::to_bytes(resp.into_body()).await.unwrap().as_ref(), br#"{"test-key":"test-value"}"# ); } #[test] fn response_builder_header_insert_kv() { let mut res = HttpResponse::Ok(); res.insert_header(("Content-Type", "application/octet-stream")); let res = res.finish(); assert_eq!( res.headers().get("Content-Type"), Some(&HeaderValue::from_static("application/octet-stream")) ); } #[test] fn response_builder_header_insert_typed() { let mut res = HttpResponse::Ok(); res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); let res = res.finish(); assert_eq!( res.headers().get("Content-Type"), Some(&HeaderValue::from_static("application/octet-stream")) ); } #[test] fn response_builder_header_append_kv() { let mut res = HttpResponse::Ok(); res.append_header(("Content-Type", "application/octet-stream")); res.append_header(("Content-Type", "application/json")); let res = res.finish(); let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); assert_eq!(headers.len(), 2); assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); assert!(headers.contains(&HeaderValue::from_static("application/json"))); } #[test] fn response_builder_header_append_typed() { let mut res = HttpResponse::Ok(); res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); let res = res.finish(); let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); assert_eq!(headers.len(), 2); assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); assert!(headers.contains(&HeaderValue::from_static("application/json"))); } } actix-web-4.9.0/src/response/customize_responder.rs000064400000000000000000000205301046102023000206170ustar 00000000000000use actix_http::{ body::EitherBody, error::HttpError, header::{HeaderMap, TryIntoHeaderPair}, StatusCode, }; use crate::{HttpRequest, HttpResponse, Responder}; /// Allows overriding status code and headers (including cookies) for a [`Responder`]. /// /// Created by calling the [`customize`](Responder::customize) method on a [`Responder`] type. pub struct CustomizeResponder { inner: CustomizeResponderInner, error: Option, } struct CustomizeResponderInner { responder: R, status: Option, override_headers: HeaderMap, append_headers: HeaderMap, } impl CustomizeResponder { pub(crate) fn new(responder: R) -> Self { CustomizeResponder { inner: CustomizeResponderInner { responder, status: None, override_headers: HeaderMap::new(), append_headers: HeaderMap::new(), }, error: None, } } /// Override a status code for the Responder's response. /// /// # Examples /// ``` /// use actix_web::{Responder, http::StatusCode, test::TestRequest}; /// /// let responder = "Welcome!".customize().with_status(StatusCode::ACCEPTED); /// /// let request = TestRequest::default().to_http_request(); /// let response = responder.respond_to(&request); /// assert_eq!(response.status(), StatusCode::ACCEPTED); /// ``` pub fn with_status(mut self, status: StatusCode) -> Self { if let Some(inner) = self.inner() { inner.status = Some(status); } self } /// Insert (override) header in the final response. /// /// Overrides other headers with the same name. /// See [`HeaderMap::insert`](crate::http::header::HeaderMap::insert). /// /// Headers added with this method will be inserted before those added /// with [`append_header`](Self::append_header). As such, header(s) can be overridden with more /// than one new header by first calling `insert_header` followed by `append_header`. /// /// # Examples /// ``` /// use actix_web::{Responder, test::TestRequest}; /// /// let responder = "Hello world!" /// .customize() /// .insert_header(("x-version", "1.2.3")); /// /// let request = TestRequest::default().to_http_request(); /// let response = responder.respond_to(&request); /// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3"); /// ``` pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { if let Some(inner) = self.inner() { match header.try_into_pair() { Ok((key, value)) => { inner.override_headers.insert(key, value); } Err(err) => self.error = Some(err.into()), }; } self } /// Append header to the final response. /// /// Unlike [`insert_header`](Self::insert_header), this will not override existing headers. /// See [`HeaderMap::append`](crate::http::header::HeaderMap::append). /// /// Headers added here are appended _after_ additions/overrides from `insert_header`. /// /// # Examples /// ``` /// use actix_web::{Responder, test::TestRequest}; /// /// let responder = "Hello world!" /// .customize() /// .append_header(("x-version", "1.2.3")); /// /// let request = TestRequest::default().to_http_request(); /// let response = responder.respond_to(&request); /// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3"); /// ``` pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { if let Some(inner) = self.inner() { match header.try_into_pair() { Ok((key, value)) => { inner.append_headers.append(key, value); } Err(err) => self.error = Some(err.into()), }; } self } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `insert_header`.")] pub fn with_header(self, header: impl TryIntoHeaderPair) -> Self where Self: Sized, { self.insert_header(header) } fn inner(&mut self) -> Option<&mut CustomizeResponderInner> { if self.error.is_some() { None } else { Some(&mut self.inner) } } /// Appends a `cookie` to the final response. /// /// # Errors /// /// Final response will be an error if `cookie` cannot be converted into a valid header value. #[cfg(feature = "cookies")] pub fn add_cookie(mut self, cookie: &crate::cookie::Cookie<'_>) -> Self { use actix_http::header::{TryIntoHeaderValue as _, SET_COOKIE}; if let Some(inner) = self.inner() { match cookie.to_string().try_into_value() { Ok(val) => { inner.append_headers.append(SET_COOKIE, val); } Err(err) => { self.error = Some(err.into()); } } } self } } impl Responder for CustomizeResponder where T: Responder, { type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { if let Some(err) = self.error { return HttpResponse::from_error(err).map_into_right_body(); } let mut res = self.inner.responder.respond_to(req); if let Some(status) = self.inner.status { *res.status_mut() = status; } for (k, v) in self.inner.override_headers { res.headers_mut().insert(k, v); } for (k, v) in self.inner.append_headers { res.headers_mut().append(k, v); } res.map_into_left_body() } } #[cfg(test)] mod tests { use actix_http::body::to_bytes; use bytes::Bytes; use super::*; use crate::{ cookie::Cookie, http::header::{HeaderValue, CONTENT_TYPE}, test::TestRequest, }; #[actix_rt::test] async fn customize_responder() { let req = TestRequest::default().to_http_request(); let res = "test" .to_string() .customize() .with_status(StatusCode::BAD_REQUEST) .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = "test" .to_string() .customize() .insert_header(("content-type", "json")) .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("json") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = "test" .to_string() .customize() .add_cookie(&Cookie::new("name", "value")) .respond_to(&req); assert!(res.status().is_success()); assert_eq!( res.cookies().collect::>>(), vec![Cookie::new("name", "value")], ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); } #[actix_rt::test] async fn tuple_responder_with_status_code() { let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::OK) .customize() .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)) .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/json") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); } } actix-web-4.9.0/src/response/http_codes.rs000064400000000000000000000106431046102023000166540ustar 00000000000000//! Status code based HTTP response builders. use actix_http::StatusCode; use crate::{HttpResponse, HttpResponseBuilder}; macro_rules! static_resp { ($name:ident, $status:expr) => { #[allow(non_snake_case, missing_docs)] pub fn $name() -> HttpResponseBuilder { HttpResponseBuilder::new($status) } }; } impl HttpResponse { static_resp!(Continue, StatusCode::CONTINUE); static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS); static_resp!(Processing, StatusCode::PROCESSING); static_resp!(Ok, StatusCode::OK); static_resp!(Created, StatusCode::CREATED); static_resp!(Accepted, StatusCode::ACCEPTED); static_resp!( NonAuthoritativeInformation, StatusCode::NON_AUTHORITATIVE_INFORMATION ); static_resp!(NoContent, StatusCode::NO_CONTENT); static_resp!(ResetContent, StatusCode::RESET_CONTENT); static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT); static_resp!(MultiStatus, StatusCode::MULTI_STATUS); static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED); static_resp!(ImUsed, StatusCode::IM_USED); static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); static_resp!(Found, StatusCode::FOUND); static_resp!(SeeOther, StatusCode::SEE_OTHER); static_resp!(NotModified, StatusCode::NOT_MODIFIED); static_resp!(UseProxy, StatusCode::USE_PROXY); static_resp!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT); static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); static_resp!(BadRequest, StatusCode::BAD_REQUEST); static_resp!(Unauthorized, StatusCode::UNAUTHORIZED); static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); static_resp!(Forbidden, StatusCode::FORBIDDEN); static_resp!(NotFound, StatusCode::NOT_FOUND); static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); static_resp!( ProxyAuthenticationRequired, StatusCode::PROXY_AUTHENTICATION_REQUIRED ); static_resp!(RequestTimeout, StatusCode::REQUEST_TIMEOUT); static_resp!(Conflict, StatusCode::CONFLICT); static_resp!(Gone, StatusCode::GONE); static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED); static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); static_resp!(UriTooLong, StatusCode::URI_TOO_LONG); static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); static_resp!(ImATeapot, StatusCode::IM_A_TEAPOT); static_resp!(MisdirectedRequest, StatusCode::MISDIRECTED_REQUEST); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); static_resp!(Locked, StatusCode::LOCKED); static_resp!(FailedDependency, StatusCode::FAILED_DEPENDENCY); static_resp!(UpgradeRequired, StatusCode::UPGRADE_REQUIRED); static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); static_resp!( RequestHeaderFieldsTooLarge, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE ); static_resp!( UnavailableForLegalReasons, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS ); static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED); static_resp!(BadGateway, StatusCode::BAD_GATEWAY); static_resp!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE); static_resp!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT); static_resp!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED); static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); static_resp!(LoopDetected, StatusCode::LOOP_DETECTED); static_resp!(NotExtended, StatusCode::NOT_EXTENDED); static_resp!( NetworkAuthenticationRequired, StatusCode::NETWORK_AUTHENTICATION_REQUIRED ); } #[cfg(test)] mod tests { use crate::{http::StatusCode, HttpResponse}; #[test] fn test_build() { let resp = HttpResponse::Ok().finish(); assert_eq!(resp.status(), StatusCode::OK); } } actix-web-4.9.0/src/response/mod.rs000064400000000000000000000004071046102023000152740ustar 00000000000000mod builder; mod customize_responder; mod http_codes; mod responder; #[allow(clippy::module_inception)] mod response; pub use self::{ builder::HttpResponseBuilder, customize_responder::CustomizeResponder, responder::Responder, response::HttpResponse, }; actix-web-4.9.0/src/response/responder.rs000064400000000000000000000267461046102023000165340ustar 00000000000000use std::borrow::Cow; use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::TryIntoHeaderPair, StatusCode, }; use bytes::{Bytes, BytesMut}; use super::CustomizeResponder; use crate::{Error, HttpRequest, HttpResponse}; /// Trait implemented by types that can be converted to an HTTP response. /// /// Any types that implement this trait can be used in the return type of a handler. Since handlers /// will only have one return type, it is idiomatic to use opaque return types `-> impl Responder`. /// /// # Implementations /// It is often not required to implement `Responder` for your own types due to a broad base of /// built-in implementations: /// - `HttpResponse` and `HttpResponseBuilder` /// - `Option` where `R: Responder` /// - `Result` where `R: Responder` and [`E: ResponseError`](crate::ResponseError) /// - `(R, StatusCode)` where `R: Responder` /// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString) /// - `&'static [u8]`, `Vec`, `Bytes`, `BytesMut` /// - [`Json`](crate::web::Json) and [`Form`](crate::web::Form) where `T: Serialize` /// - [`Either`](crate::web::Either) where `L: Serialize` and `R: Serialize` /// - [`CustomizeResponder`] /// - [`actix_files::NamedFile`](https://docs.rs/actix-files/latest/actix_files/struct.NamedFile.html) /// - [Experimental responders from `actix-web-lab`](https://docs.rs/actix-web-lab/latest/actix_web_lab/respond/index.html) /// - Third party integrations may also have implemented `Responder` where appropriate. For example, /// HTML templating engines. /// /// # Customizing Responder Output /// Calling [`.customize()`](Responder::customize) on any responder type will wrap it in a /// [`CustomizeResponder`] capable of overriding various parts of the response such as the status /// code and header map. pub trait Responder { type Body: MessageBody + 'static; /// Convert self to `HttpResponse`. fn respond_to(self, req: &HttpRequest) -> HttpResponse; /// Wraps responder to allow alteration of its response. /// /// See [`CustomizeResponder`] docs for more details on its capabilities. /// /// # Examples /// ``` /// use actix_web::{Responder, http::StatusCode, test::TestRequest}; /// /// let responder = "Hello world!" /// .customize() /// .with_status(StatusCode::BAD_REQUEST) /// .insert_header(("x-hello", "world")); /// /// let request = TestRequest::default().to_http_request(); /// let response = responder.respond_to(&request); /// assert_eq!(response.status(), StatusCode::BAD_REQUEST); /// assert_eq!(response.headers().get("x-hello").unwrap(), "world"); /// ``` #[inline] fn customize(self) -> CustomizeResponder where Self: Sized, { CustomizeResponder::new(self) } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Prefer `.customize().with_status(header)`.")] fn with_status(self, status: StatusCode) -> CustomizeResponder where Self: Sized, { self.customize().with_status(status) } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")] fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder where Self: Sized, { self.customize().insert_header(header) } } impl Responder for actix_http::Response { type Body = BoxBody; #[inline] fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from(self) } } impl Responder for actix_http::ResponseBuilder { type Body = BoxBody; #[inline] fn respond_to(mut self, req: &HttpRequest) -> HttpResponse { self.finish().map_into_boxed_body().respond_to(req) } } impl Responder for Option { type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { Some(val) => val.respond_to(req).map_into_left_body(), None => HttpResponse::new(StatusCode::NOT_FOUND).map_into_right_body(), } } } impl Responder for Result where R: Responder, E: Into, { type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { Ok(val) => val.respond_to(req).map_into_left_body(), Err(err) => HttpResponse::from_error(err.into()).map_into_right_body(), } } } impl Responder for (R, StatusCode) { type Body = R::Body; fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); *res.status_mut() = self.1; res } } macro_rules! impl_responder_by_forward_into_base_response { ($res:ty, $body:ty) => { impl Responder for $res { type Body = $body; fn respond_to(self, _: &HttpRequest) -> HttpResponse { let res: actix_http::Response<_> = self.into(); res.into() } } }; ($res:ty) => { impl_responder_by_forward_into_base_response!($res, $res); }; } impl_responder_by_forward_into_base_response!(&'static [u8]); impl_responder_by_forward_into_base_response!(Vec); impl_responder_by_forward_into_base_response!(Bytes); impl_responder_by_forward_into_base_response!(BytesMut); impl_responder_by_forward_into_base_response!(&'static str); impl_responder_by_forward_into_base_response!(String); impl_responder_by_forward_into_base_response!(bytestring::ByteString); macro_rules! impl_into_string_responder { ($res:ty) => { impl Responder for $res { type Body = String; fn respond_to(self, _: &HttpRequest) -> HttpResponse { let string: String = self.into(); let res: actix_http::Response<_> = string.into(); res.into() } } }; } impl_into_string_responder!(&'_ String); impl_into_string_responder!(Cow<'_, str>); #[cfg(test)] pub(crate) mod tests { use actix_http::body::to_bytes; use actix_service::Service; use super::*; use crate::{ error, http::header::{HeaderValue, CONTENT_TYPE}, test::{assert_body_eq, init_service, TestRequest}, web, App, }; #[actix_rt::test] async fn test_option_responder() { let srv = init_service( App::new() .service(web::resource("/none").to(|| async { Option::<&'static str>::None })) .service(web::resource("/some").to(|| async { Some("some") })), ) .await; let req = TestRequest::with_uri("/none").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/some").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_body_eq!(resp, b"some"); } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); let res = "test".respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = b"test".respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = "test".to_string().respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = (&"test".to_string()).respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let s = String::from("test"); let res = Cow::Borrowed(s.as_str()).respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = Cow::<'_, str>::Owned(s).respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = Cow::Borrowed("test").respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = Bytes::from_static(b"test").respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = BytesMut::from(b"test".as_ref()).respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); assert_eq!( to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); // InternalError let res = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_result_responder() { let req = TestRequest::default().to_http_request(); // Result let resp = Ok::<_, Error>("test".to_string()).respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!( to_bytes(resp.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); let res = Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } } actix-web-4.9.0/src/response/response.rs000064400000000000000000000304361046102023000163600ustar 00000000000000use std::{ cell::{Ref, RefMut}, fmt, }; use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::HeaderMap, Extensions, Response, ResponseHead, StatusCode, }; #[cfg(feature = "cookies")] use { actix_http::{ error::HttpError, header::{self, HeaderValue}, }, cookie::Cookie, }; use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder}; /// An outgoing response. pub struct HttpResponse { res: Response, error: Option, } impl HttpResponse { /// Constructs a response. #[inline] pub fn new(status: StatusCode) -> Self { Self { res: Response::new(status), error: None, } } /// Constructs a response builder with specific HTTP status. #[inline] pub fn build(status: StatusCode) -> HttpResponseBuilder { HttpResponseBuilder::new(status) } /// Create an error response. #[inline] pub fn from_error(error: impl Into) -> Self { let error = error.into(); let mut response = error.as_response_error().error_response(); response.error = Some(error); response } } impl HttpResponse { /// Constructs a response with body #[inline] pub fn with_body(status: StatusCode, body: B) -> Self { Self { res: Response::with_body(status, body), error: None, } } /// Returns a reference to response head. #[inline] pub fn head(&self) -> &ResponseHead { self.res.head() } /// Returns a mutable reference to response head. #[inline] pub fn head_mut(&mut self) -> &mut ResponseHead { self.res.head_mut() } /// The source `error` for this response #[inline] pub fn error(&self) -> Option<&Error> { self.error.as_ref() } /// Get the response status code #[inline] pub fn status(&self) -> StatusCode { self.res.status() } /// Set the `StatusCode` for this response #[inline] pub fn status_mut(&mut self) -> &mut StatusCode { self.res.status_mut() } /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { self.res.headers() } /// Get a mutable reference to the headers #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { self.res.headers_mut() } /// Get an iterator for the cookies set by this response. #[cfg(feature = "cookies")] pub fn cookies(&self) -> CookieIter<'_> { CookieIter { iter: self.headers().get_all(header::SET_COOKIE), } } /// Add a cookie to this response. /// /// # Errors /// Returns an error if the cookie results in a malformed `Set-Cookie` header. #[cfg(feature = "cookies")] pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { HeaderValue::from_str(&cookie.to_string()) .map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie)) .map_err(Into::into) } /// Add a "removal" cookie to the response that matches attributes of given cookie. /// /// This will cause browsers/clients to remove stored cookies with this name. /// /// The `Set-Cookie` header added to the response will have: /// - name matching given cookie; /// - domain matching given cookie; /// - path matching given cookie; /// - an empty value; /// - a max-age of `0`; /// - an expiration date far in the past. /// /// If the cookie you're trying to remove has an explicit path or domain set, those attributes /// will need to be included in the cookie passed in here. /// /// # Errors /// Returns an error if the given name results in a malformed `Set-Cookie` header. #[cfg(feature = "cookies")] pub fn add_removal_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { let mut removal_cookie = cookie.to_owned(); removal_cookie.make_removal(); HeaderValue::from_str(&removal_cookie.to_string()) .map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie)) .map_err(Into::into) } /// Remove all cookies with the given name from this response. /// /// Returns the number of cookies removed. /// /// This method can _not_ cause a browser/client to delete any of its stored cookies. Its only /// purpose is to delete cookies that were added to this response using [`add_cookie`] /// and [`add_removal_cookie`]. Use [`add_removal_cookie`] to send a "removal" cookie. /// /// [`add_cookie`]: Self::add_cookie /// [`add_removal_cookie`]: Self::add_removal_cookie #[cfg(feature = "cookies")] pub fn del_cookie(&mut self, name: &str) -> usize { let headers = self.headers_mut(); let vals: Vec = headers .get_all(header::SET_COOKIE) .map(|v| v.to_owned()) .collect(); headers.remove(header::SET_COOKIE); let mut count: usize = 0; for v in vals { if let Ok(s) = v.to_str() { if let Ok(c) = Cookie::parse_encoded(s) { if c.name() == name { count += 1; continue; } } } // put set-cookie header head back if it does not validate headers.append(header::SET_COOKIE, v); } count } /// Connection upgrade status #[inline] pub fn upgrade(&self) -> bool { self.res.upgrade() } /// Keep-alive status for this connection pub fn keep_alive(&self) -> bool { self.res.keep_alive() } /// Returns reference to the response-local data/extensions container. #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { self.res.extensions() } /// Returns reference to the response-local data/extensions container. #[inline] pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { self.res.extensions_mut() } /// Returns a reference to this response's body. #[inline] pub fn body(&self) -> &B { self.res.body() } /// Sets new body. pub fn set_body(self, body: B2) -> HttpResponse { HttpResponse { res: self.res.set_body(body), error: self.error, } } /// Returns split head and body. /// /// # Implementation Notes /// Due to internal performance optimizations, the first element of the returned tuple is an /// `HttpResponse` as well but only contains the head of the response this was called on. pub fn into_parts(self) -> (HttpResponse<()>, B) { let (head, body) = self.res.into_parts(); ( HttpResponse { res: head, error: self.error, }, body, ) } /// Drops body and returns new response. pub fn drop_body(self) -> HttpResponse<()> { HttpResponse { res: self.res.drop_body(), error: self.error, } } /// Map the current body type to another using a closure, returning a new response. /// /// Closure receives the response head and the current body type. pub fn map_body(self, f: F) -> HttpResponse where F: FnOnce(&mut ResponseHead, B) -> B2, { HttpResponse { res: self.res.map_body(f), error: self.error, } } /// Map the current body type `B` to `EitherBody::Left(B)`. /// /// Useful for middleware which can generate their own responses. #[inline] pub fn map_into_left_body(self) -> HttpResponse> { self.map_body(|_, body| EitherBody::left(body)) } /// Map the current body type `B` to `EitherBody::Right(B)`. /// /// Useful for middleware which can generate their own responses. #[inline] pub fn map_into_right_body(self) -> HttpResponse> { self.map_body(|_, body| EitherBody::right(body)) } /// Map the current body to a type-erased `BoxBody`. #[inline] pub fn map_into_boxed_body(self) -> HttpResponse where B: MessageBody + 'static, { self.map_body(|_, body| body.boxed()) } /// Returns the response body, dropping all other parts. pub fn into_body(self) -> B { self.res.into_body() } } impl fmt::Debug for HttpResponse where B: MessageBody, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpResponse") .field("error", &self.error) .field("res", &self.res) .finish() } } impl From> for HttpResponse { fn from(res: Response) -> Self { HttpResponse { res, error: None } } } impl From for HttpResponse { fn from(err: Error) -> Self { HttpResponse::from_error(err) } } impl From> for Response { fn from(res: HttpResponse) -> Self { // this impl will always be called as part of dispatcher res.res } } // Rationale for cfg(test): this impl causes false positives on a clippy lint (async_yields_async) // when returning an HttpResponse from an async function/closure and it's not very useful outside of // tests anyway. #[cfg(test)] mod response_fut_impl { use std::{ future::Future, mem, pin::Pin, task::{Context, Poll}, }; use super::*; // Future is only implemented for BoxBody payload type because it's the most useful for making // simple handlers without async blocks. Making it generic over all MessageBody types requires a // future impl on Response which would cause its body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. impl Future for HttpResponse { type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { if let Some(err) = self.error.take() { return Poll::Ready(Err(err)); } Poll::Ready(Ok(mem::replace( &mut self.res, Response::new(StatusCode::default()), ))) } } } impl Responder for HttpResponse where B: MessageBody + 'static, { type Body = B; #[inline] fn respond_to(self, _: &HttpRequest) -> HttpResponse { self } } #[cfg(feature = "cookies")] pub struct CookieIter<'a> { iter: std::slice::Iter<'a, HeaderValue>, } #[cfg(feature = "cookies")] impl<'a> Iterator for CookieIter<'a> { type Item = Cookie<'a>; #[inline] fn next(&mut self) -> Option> { for v in self.iter.by_ref() { if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { return Some(c); } } None } } #[cfg(test)] mod tests { use static_assertions::assert_impl_all; use super::*; use crate::http::header::COOKIE; assert_impl_all!(HttpResponse: Responder); assert_impl_all!(HttpResponse: Responder); assert_impl_all!(HttpResponse<&'static str>: Responder); assert_impl_all!(HttpResponse: Responder); #[test] fn test_debug() { let resp = HttpResponse::Ok() .append_header((COOKIE, HeaderValue::from_static("cookie1=value1; "))) .append_header((COOKIE, HeaderValue::from_static("cookie2=value2; "))) .finish(); let dbg = format!("{:?}", resp); assert!(dbg.contains("HttpResponse")); } } #[cfg(test)] #[cfg(feature = "cookies")] mod cookie_tests { use super::*; #[test] fn removal_cookies() { let mut res = HttpResponse::Ok().finish(); let cookie = Cookie::new("foo", ""); res.add_removal_cookie(&cookie).unwrap(); let set_cookie_hdr = res.headers().get(header::SET_COOKIE).unwrap(); assert_eq!( &set_cookie_hdr.as_bytes()[..25], &b"foo=; Max-Age=0; Expires="[..], "unexpected set-cookie value: {:?}", set_cookie_hdr.to_str() ); } } actix-web-4.9.0/src/rmap.rs000064400000000000000000000443701046102023000136250ustar 00000000000000use std::{ borrow::Cow, cell::RefCell, fmt::Write as _, rc::{Rc, Weak}, }; use actix_router::ResourceDef; use ahash::AHashMap; use url::Url; use crate::{error::UrlGenerationError, request::HttpRequest}; const AVG_PATH_LEN: usize = 24; #[derive(Clone, Debug)] pub struct ResourceMap { pattern: ResourceDef, /// Named resources within the tree or, for external resources, it points to isolated nodes /// outside the tree. named: AHashMap>, parent: RefCell>, /// Must be `None` for "edge" nodes. nodes: Option>>, } impl ResourceMap { /// Creates a _container_ node in the `ResourceMap` tree. pub fn new(root: ResourceDef) -> Self { ResourceMap { pattern: root, named: AHashMap::default(), parent: RefCell::new(Weak::new()), nodes: Some(Vec::new()), } } /// Format resource map as tree structure (unfinished). #[allow(dead_code)] pub(crate) fn tree(&self) -> String { let mut buf = String::new(); self._tree(&mut buf, 0); buf } pub(crate) fn _tree(&self, buf: &mut String, level: usize) { if let Some(children) = &self.nodes { for child in children { writeln!( buf, "{}{} {}", "--".repeat(level), child.pattern.pattern().unwrap(), child .pattern .name() .map(|name| format!("({})", name)) .unwrap_or_else(|| "".to_owned()) ) .unwrap(); ResourceMap::_tree(child, buf, level + 1); } } } /// Adds a (possibly nested) resource. /// /// To add a non-prefix pattern, `nested` must be `None`. /// To add external resource, supply a pattern without a leading `/`. /// The root pattern of `nested`, if present, should match `pattern`. pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { pattern.set_id(self.nodes.as_ref().unwrap().len() as u16); if let Some(new_node) = nested { debug_assert_eq!( &new_node.pattern, pattern, "`pattern` and `nested` mismatch" ); // parents absorb references to the named resources of children self.named.extend(new_node.named.clone()); self.nodes.as_mut().unwrap().push(new_node); } else { let new_node = Rc::new(ResourceMap { pattern: pattern.clone(), named: AHashMap::default(), parent: RefCell::new(Weak::new()), nodes: None, }); if let Some(name) = pattern.name() { self.named.insert(name.to_owned(), Rc::clone(&new_node)); } let is_external = match pattern.pattern() { Some(p) => !p.is_empty() && !p.starts_with('/'), None => false, }; // don't add external resources to the tree if !is_external { self.nodes.as_mut().unwrap().push(new_node); } } } pub(crate) fn finish(self: &Rc) { for node in self.nodes.iter().flatten() { node.parent.replace(Rc::downgrade(self)); ResourceMap::finish(node); } } /// Generate URL for named resource. /// /// Check [`HttpRequest::url_for`] for detailed information. pub fn url_for( &self, req: &HttpRequest, name: &str, elements: U, ) -> Result where U: IntoIterator, I: AsRef, { let mut elements = elements.into_iter(); let path = self .named .get(name) .ok_or(UrlGenerationError::ResourceNotFound)? .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| { node.pattern .resource_path_from_iter(&mut acc, &mut elements) .then_some(acc) }) .ok_or(UrlGenerationError::NotEnoughElements)?; let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { // build full URL from connection info parts and resource path let conn = req.connection_info(); let base = format!("{}://{}", conn.scheme(), conn.host()); (Cow::Owned(base), path.as_str()) } else { // external resource; third slash would be the root slash in the path let third_slash_index = path .char_indices() .filter_map(|(i, c)| (c == '/').then_some(i)) .nth(2) .unwrap_or(path.len()); ( Cow::Borrowed(&path[..third_slash_index]), &path[third_slash_index..], ) }; let mut url = Url::parse(&base)?; url.set_path(path); Ok(url) } /// Returns true if there is a resource that would match `path`. pub fn has_resource(&self, path: &str) -> bool { self.find_matching_node(path).is_some() } /// Returns the name of the route that matches the given path or None if no full match /// is possible or the matching resource is not named. pub fn match_name(&self, path: &str) -> Option<&str> { self.find_matching_node(path)?.pattern.name() } /// Returns the full resource pattern matched against a path or None if no full match /// is possible. pub fn match_pattern(&self, path: &str) -> Option { self.find_matching_node(path)?.root_rmap_fn( String::with_capacity(AVG_PATH_LEN), |mut acc, node| { let pattern = node.pattern.pattern()?; acc.push_str(pattern); Some(acc) }, ) } fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> { self._find_matching_node(path).flatten() } /// Returns `None` if root pattern doesn't match; /// `Some(None)` if root pattern matches but there is no matching child pattern. /// Don't search sideways when `Some(none)` is returned. fn _find_matching_node(&self, path: &str) -> Option> { let matched_len = self.pattern.find_match(path)?; let path = &path[matched_len..]; Some(match &self.nodes { // find first sub-node to match remaining path Some(nodes) => nodes .iter() .filter_map(|node| node._find_matching_node(path)) .next() .flatten(), // only terminate at edge nodes None => Some(self), }) } /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context. fn root_rmap_fn(&self, init: B, mut f: F) -> Option where F: FnMut(B, &ResourceMap) -> Option, { self._root_rmap_fn(init, &mut f) } /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map. fn _root_rmap_fn(&self, init: B, f: &mut F) -> Option where F: FnMut(B, &ResourceMap) -> Option, { let data = match self.parent.borrow().upgrade() { Some(ref parent) => parent._root_rmap_fn(init, f)?, None => init, }; f(data, self) } } #[cfg(test)] mod tests { use super::*; #[test] fn extract_matched_pattern() { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); user_map.add( &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), None, ); root.add(&mut ResourceDef::new("/info"), None); root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None); root.add( &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); root.add(&mut ResourceDef::new("/info"), None); let root = Rc::new(root); ResourceMap::finish(&root); // sanity check resource map setup assert!(root.has_resource("/info")); assert!(!root.has_resource("/bar")); assert!(root.has_resource("/v1")); assert!(root.has_resource("/v2")); assert!(!root.has_resource("/v33")); assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/profile")); // extract patterns from paths assert!(root.match_pattern("/bar").is_none()); assert!(root.match_pattern("/v44").is_none()); assert_eq!(root.match_pattern("/info"), Some("/info".to_owned())); assert_eq!( root.match_pattern("/v1"), Some("/v{version:[[:digit:]]{1}}".to_owned()) ); assert_eq!( root.match_pattern("/v2"), Some("/v{version:[[:digit:]]{1}}".to_owned()) ); assert_eq!( root.match_pattern("/user/22/profile"), Some("/user/{id}/profile".to_owned()) ); assert_eq!( root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"), Some("/user/{id}/profile".to_owned()) ); assert_eq!( root.match_pattern("/user/22/article/44"), Some("/user/{id}/article/{id}".to_owned()) ); assert_eq!( root.match_pattern("/user/22/post/my-post"), Some("/user/{id}/post/{post_id}".to_owned()) ); assert_eq!( root.match_pattern("/user/22/post/other-post/comment/42"), Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) ); } #[test] fn extract_matched_name() { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut rdef = ResourceDef::new("/info"); rdef.set_name("root_info"); root.add(&mut rdef, None); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); let mut rdef = ResourceDef::new("/"); user_map.add(&mut rdef, None); let mut rdef = ResourceDef::new("/post/{post_id}"); rdef.set_name("user_post"); user_map.add(&mut rdef, None); root.add( &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); let root = Rc::new(root); ResourceMap::finish(&root); // sanity check resource map setup assert!(root.has_resource("/info")); assert!(!root.has_resource("/bar")); assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/post/55")); // extract patterns from paths assert!(root.match_name("/bar").is_none()); assert!(root.match_name("/v44").is_none()); assert_eq!(root.match_name("/info"), Some("root_info")); assert_eq!(root.match_name("/user/22"), None); assert_eq!(root.match_name("/user/22/"), None); assert_eq!(root.match_name("/user/22/post/55"), Some("user_post")); } #[test] fn bug_fix_issue_1582_debug_print_exits() { // ref: https://github.com/actix/actix-web/issues/1582 let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); user_map.add( &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), None, ); root.add( &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); let root = Rc::new(root); ResourceMap::finish(&root); // check root has no parent assert!(root.parent.borrow().upgrade().is_none()); // check child has parent reference assert!(root.nodes.as_ref().unwrap()[0] .parent .borrow() .upgrade() .is_some()); // check child's parent root id matches root's root id assert!(Rc::ptr_eq( &root.nodes.as_ref().unwrap()[0] .parent .borrow() .upgrade() .unwrap(), &root )); let output = format!("{:?}", root); assert!(output.starts_with("ResourceMap {")); assert!(output.ends_with(" }")); } #[test] fn short_circuit() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut user_root = ResourceDef::prefix("/user"); let mut user_map = ResourceMap::new(user_root.clone()); user_map.add(&mut ResourceDef::new("/u1"), None); user_map.add(&mut ResourceDef::new("/u2"), None); root.add(&mut ResourceDef::new("/user/u3"), None); root.add(&mut user_root, Some(Rc::new(user_map))); root.add(&mut ResourceDef::new("/user/u4"), None); let rmap = Rc::new(root); ResourceMap::finish(&rmap); assert!(rmap.has_resource("/user/u1")); assert!(rmap.has_resource("/user/u2")); assert!(rmap.has_resource("/user/u3")); assert!(!rmap.has_resource("/user/u4")); } #[test] fn url_for() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut user_scope_rdef = ResourceDef::prefix("/user"); let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone()); let mut user_rdef = ResourceDef::new("/{user_id}"); let mut user_map = ResourceMap::new(user_rdef.clone()); let mut post_rdef = ResourceDef::new("/post/{sub_id}"); post_rdef.set_name("post"); user_map.add(&mut post_rdef, None); user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map))); root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map))); let rmap = Rc::new(root); ResourceMap::finish(&rmap); let mut req = crate::test::TestRequest::default(); req.set_server_hostname("localhost:8888"); let req = req.to_http_request(); let url = rmap .url_for(&req, "post", ["u123", "foobar"]) .unwrap() .to_string(); assert_eq!(url, "http://localhost:8888/user/u123/post/foobar"); assert!(rmap.url_for(&req, "missing", ["u123"]).is_err()); } #[test] fn url_for_parser() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut rdef_1 = ResourceDef::new("/{var}"); rdef_1.set_name("internal"); let mut rdef_2 = ResourceDef::new("http://host.dom/{var}"); rdef_2.set_name("external.1"); let mut rdef_3 = ResourceDef::new("{var}"); rdef_3.set_name("external.2"); root.add(&mut rdef_1, None); root.add(&mut rdef_2, None); root.add(&mut rdef_3, None); let rmap = Rc::new(root); ResourceMap::finish(&rmap); let mut req = crate::test::TestRequest::default(); req.set_server_hostname("localhost:8888"); let req = req.to_http_request(); const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"]; const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag"; let url = rmap.url_for(&req, "internal", INPUT).unwrap(); assert_eq!(url.path(), OUTPUT); let url = rmap.url_for(&req, "external.1", INPUT).unwrap(); assert_eq!(url.path(), OUTPUT); assert!(rmap.url_for(&req, "external.2", INPUT).is_err()); assert!(rmap.url_for(&req, "external.2", [""]).is_err()); } #[test] fn external_resource_with_no_name() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut rdef = ResourceDef::new("https://duck.com/{query}"); root.add(&mut rdef, None); let rmap = Rc::new(root); ResourceMap::finish(&rmap); assert!(!rmap.has_resource("https://duck.com/abc")); } #[test] fn external_resource_with_name() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut rdef = ResourceDef::new("https://duck.com/{query}"); rdef.set_name("duck"); root.add(&mut rdef, None); let rmap = Rc::new(root); ResourceMap::finish(&rmap); assert!(!rmap.has_resource("https://duck.com/abc")); let mut req = crate::test::TestRequest::default(); req.set_server_hostname("localhost:8888"); let req = req.to_http_request(); assert_eq!( rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(), "https://duck.com/abcd" ); } #[test] fn url_for_override_within_map() { let mut root = ResourceMap::new(ResourceDef::prefix("")); let mut foo_rdef = ResourceDef::prefix("/foo"); let mut foo_map = ResourceMap::new(foo_rdef.clone()); let mut nested_rdef = ResourceDef::new("/nested"); nested_rdef.set_name("nested"); foo_map.add(&mut nested_rdef, None); root.add(&mut foo_rdef, Some(Rc::new(foo_map))); let mut foo_rdef = ResourceDef::prefix("/bar"); let mut foo_map = ResourceMap::new(foo_rdef.clone()); let mut nested_rdef = ResourceDef::new("/nested"); nested_rdef.set_name("nested"); foo_map.add(&mut nested_rdef, None); root.add(&mut foo_rdef, Some(Rc::new(foo_map))); let rmap = Rc::new(root); ResourceMap::finish(&rmap); let req = crate::test::TestRequest::default().to_http_request(); let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string(); assert_eq!(url, "http://localhost:8080/bar/nested"); assert!(rmap.url_for(&req, "missing", ["u123"]).is_err()); } } actix-web-4.9.0/src/route.rs000064400000000000000000000351211046102023000140160ustar 00000000000000use std::{mem, rc::Rc}; use actix_http::{body::MessageBody, Method}; use actix_service::{ apply, boxed::{self, BoxService}, fn_service, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use crate::{ guard::{self, Guard}, handler::{handler_service, Handler}, middleware::Compat, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, Error, FromRequest, HttpResponse, Responder, }; /// A request handler with [guards](guard). /// /// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` /// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, } impl Route { /// Create new route which matches any request. #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { service: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::NotFound())) })), guards: Rc::new(Vec::new()), } } /// Registers a route middleware. /// /// `mw` is a middleware component (type), that can modify the requests and responses handled by /// this `Route`. /// /// See [`App::wrap`](crate::App::wrap) for more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap(self, mw: M) -> Route where M: Transform< BoxService, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody + 'static, { Route { service: boxed::factory(apply(Compat::new(mw), self.service)), guards: self.guards, } } pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } impl ServiceFactory for Route { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = RouteService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { let fut = self.service.new_service(()); let guards = Rc::clone(&self.guards); Box::pin(async move { let service = fut.await?; Ok(RouteService { service, guards }) }) } } pub struct RouteService { service: BoxService, guards: Rc>>, } impl RouteService { // TODO(breaking): remove pass by ref mut #[allow(clippy::needless_pass_by_ref_mut)] pub fn check(&self, req: &mut ServiceRequest) -> bool { let guard_ctx = req.guard_ctx(); for guard in self.guards.iter() { if !guard.check(&guard_ctx) { return false; } } true } } impl Service for RouteService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { self.service.call(req) } } impl Route { /// Add method guard to the route. /// /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( /// web::get() /// .method(http::Method::CONNECT) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) /// ); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { Rc::get_mut(&mut self.guards) .unwrap() .push(Box::new(guard::Method(method))); self } /// Add guard to the route. /// /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( /// web::route() /// .guard(guard::Get()) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) /// ); /// # } /// ``` pub fn guard(mut self, f: F) -> Self { Rc::get_mut(&mut self.guards).unwrap().push(Box::new(f)); self } /// Set handler function, use request extractors for parameters. /// /// # Examples /// ``` /// use actix_web::{web, http, App}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// extract path info using serde /// async fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.username) /// } /// /// let app = App::new().service( /// web::resource("/{username}/index.html") // <- define path parameters /// .route(web::get().to(index)) // <- register handler /// ); /// ``` /// /// It is possible to use multiple extractors for one handler function. /// ``` /// # use std::collections::HashMap; /// # use serde::Deserialize; /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// extract path info using serde /// async fn index( /// path: web::Path, /// query: web::Query>, /// body: web::Json /// ) -> String { /// format!("Welcome {}!", path.username) /// } /// /// let app = App::new().service( /// web::resource("/{username}/index.html") // <- define path parameters /// .route(web::get().to(index)) /// ); /// ``` pub fn to(mut self, handler: F) -> Self where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, { self.service = handler_service(handler); self } /// Set raw service to be constructed and called as the request handler. /// /// # Examples /// ``` /// # use std::convert::Infallible; /// # use futures_util::future::LocalBoxFuture; /// # use actix_web::{*, dev::*, http::header}; /// struct HelloWorld; /// /// impl Service for HelloWorld { /// type Response = ServiceResponse; /// type Error = Infallible; /// type Future = LocalBoxFuture<'static, Result>; /// /// dev::always_ready!(); /// /// fn call(&self, req: ServiceRequest) -> Self::Future { /// let (req, _) = req.into_parts(); /// /// let res = HttpResponse::Ok() /// .insert_header(header::ContentType::plaintext()) /// .body("Hello world!"); /// /// Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) /// } /// } /// /// App::new().route( /// "/", /// web::get().service(fn_factory(|| async { Ok(HelloWorld) })), /// ); /// ``` pub fn service(mut self, service_factory: S) -> Self where S: ServiceFactory< ServiceRequest, Response = ServiceResponse, Error = E, InitError = (), Config = (), > + 'static, E: Into + 'static, { self.service = boxed::factory(service_factory.map_err(Into::into)); self } } #[cfg(test)] mod tests { use std::{convert::Infallible, time::Duration}; use actix_rt::time::sleep; use bytes::Bytes; use futures_core::future::LocalBoxFuture; use serde::Serialize; use crate::{ dev::{always_ready, fn_factory, fn_service, Service}, error, http::{header, Method, StatusCode}, middleware::{DefaultHeaders, Logger}, service::{ServiceRequest, ServiceResponse}, test::{call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, }; #[derive(Serialize, PartialEq, Debug)] struct MyObject { name: String, } #[actix_rt::test] async fn test_route() { let srv = init_service( App::new() .service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .route(web::put().to(|| async { Err::(error::ErrorBadRequest("err")) })) .route(web::post().to(|| async { sleep(Duration::from_millis(100)).await; Ok::<_, Infallible>(HttpResponse::Created()) })) .route(web::delete().to(|| async { sleep(Duration::from_millis(100)).await; Err::(error::ErrorBadRequest("err")) })), ) .service(web::resource("/json").route(web::get().to(|| async { sleep(Duration::from_millis(25)).await; web::Json(MyObject { name: "test".to_string(), }) }))), ) .await; let req = TestRequest::with_uri("/test") .method(Method::GET) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/test") .method(Method::PUT) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::DELETE) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::HEAD) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/json").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } #[actix_rt::test] async fn route_middleware() { let srv = init_service( App::new() .route("/", web::get().to(HttpResponse::Ok).wrap(Logger::default())) .service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .route( web::post() .to(HttpResponse::Created) .wrap(DefaultHeaders::new().add(("x-test", "x-posted"))), ) .route( web::delete() .to(HttpResponse::Accepted) // logger changes body type, proving Compat is not needed .wrap(Logger::default()), ), ), ) .await; let req = TestRequest::get().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key("x-test")); let req = TestRequest::post().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::CREATED); assert_eq!(res.headers().get("x-test").unwrap(), "x-posted"); let req = TestRequest::delete().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::ACCEPTED); } #[actix_rt::test] async fn test_service_handler() { struct HelloWorld; impl Service for HelloWorld { type Response = ServiceResponse; type Error = crate::Error; type Future = LocalBoxFuture<'static, Result>; always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, _) = req.into_parts(); let res = HttpResponse::Ok() .insert_header(header::ContentType::plaintext()) .body("Hello world!"); Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) } } let srv = init_service( App::new() .route( "/hello", web::get().service(fn_factory(|| async { Ok(HelloWorld) })), ) .route( "/bye", web::get().service(fn_factory(|| async { Ok::<_, ()>(fn_service(|req: ServiceRequest| async { let (req, _) = req.into_parts(); let res = HttpResponse::Ok() .insert_header(header::ContentType::plaintext()) .body("Goodbye, and thanks for all the fish!"); Ok::<_, Infallible>(ServiceResponse::new(req, res)) })) })), ), ) .await; let req = TestRequest::get().uri("/hello").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"Hello world!")); let req = TestRequest::get().uri("/bye").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!( body, Bytes::from_static(b"Goodbye, and thanks for all the fish!") ); } } actix-web-4.9.0/src/rt.rs000064400000000000000000000055771046102023000133210ustar 00000000000000//! A selection of re-exports from [`tokio`] and [`actix-rt`]. //! //! Actix Web runs on [Tokio], providing full[^compat] compatibility with its huge ecosystem of //! crates. Each of the server's workers uses a single-threaded runtime. Read more about the //! architecture in [`actix-rt`]'s docs. //! //! # Running Actix Web Without Macros //! //! ```no_run //! use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; //! //! async fn index(req: HttpRequest) -> &'static str { //! println!("REQ: {:?}", req); //! "Hello world!\r\n" //! } //! //! fn main() -> std::io::Result<()> { //! rt::System::new().block_on( //! HttpServer::new(|| { //! App::new().service(web::resource("/").route(web::get().to(index))) //! }) //! .bind(("127.0.0.1", 8080))? //! .run() //! ) //! } //! ``` //! //! # Running Actix Web Using `#[tokio::main]` //! //! If you need to run something that uses Tokio's work stealing functionality alongside Actix Web, //! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned //! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred. //! //! Note that `actix` actor support (and therefore WebSocket support through `actix-web-actors`) //! still require `#[actix_web::main]` since they require a [`System`] to be set up. //! //! Also note that calls to this module's [`spawn()`] re-export require an `#[actix_web::main]` //! runtime (or a manually configured `LocalSet`) since it makes calls into to the current thread's //! `LocalSet`, which `#[tokio::main]` does not set up. //! //! ```no_run //! use actix_web::{get, middleware, rt, web, App, HttpRequest, HttpServer}; //! //! #[get("/")] //! async fn index(req: HttpRequest) -> &'static str { //! println!("REQ: {:?}", req); //! "Hello world!\r\n" //! } //! //! #[tokio::main] //! async fn main() -> std::io::Result<()> { //! HttpServer::new(|| { //! App::new().service(index) //! }) //! .bind(("127.0.0.1", 8080))? //! .run() //! .await //! } //! ``` //! //! [^compat]: Crates that use Tokio's [`block_in_place`] will not work with Actix Web. Fortunately, //! the vast majority of Tokio-based crates do not use it. //! //! [`actix-rt`]: https://docs.rs/actix-rt //! [`tokio`]: https://docs.rs/tokio //! [Tokio]: https://docs.rs/tokio //! [`spawn`]: https://docs.rs/tokio/1/tokio/fn.spawn.html //! [`block_in_place`]: https://docs.rs/tokio/1/tokio/task/fn.block_in_place.html // In particular: // - Omit the `Arbiter` types because they have limited value here. // - Re-export but hide the runtime macros because they won't work directly but are required for // `#[actix_web::main]` and `#[actix_web::test]` to work. #[cfg(feature = "macros")] #[doc(hidden)] pub use actix_macros::{main, test}; pub use actix_rt::{net, pin, signal, spawn, task, time, Runtime, System, SystemRunner}; actix-web-4.9.0/src/scope.rs000064400000000000000000001215611046102023000137750ustar 00000000000000use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::{body::MessageBody, Extensions}; use actix_router::{ResourceDef, Router}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ config::ServiceConfig, data::Data, dev::AppService, guard::Guard, rmap::ResourceMap, service::{ AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }, Error, Resource, Route, }; type Guards = Vec>; /// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix. /// /// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from /// requests using the [`Path`](crate::web::Path) extractor or /// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info). /// /// # Avoid Trailing Slashes /// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost /// certainly not have the expected behavior. See the [documentation on resource definitions][pat] /// to understand why this is the case and how to correctly construct scope/prefix definitions. /// /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( /// web::scope("/{project_id}") /// .service(web::resource("/path1").to(|| async { "OK" })) /// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) /// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) /// ); /// ``` /// /// In the above example three routes get registered: /// - /{project_id}/path1 - responds to all HTTP methods /// - /{project_id}/path2 - responds to `GET` requests /// - /{project_id}/path3 - responds to `HEAD` requests /// /// [pat]: crate::dev::ResourceDef#prefix-resources /// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments pub struct Scope { endpoint: T, rdef: String, app_data: Option, services: Vec>, guards: Vec>, default: Option>, external: Vec, factory_ref: Rc>>, } impl Scope { /// Create a new scope pub fn new(path: &str) -> Scope { let factory_ref = Rc::new(RefCell::new(None)); Scope { endpoint: ScopeEndpoint::new(Rc::clone(&factory_ref)), rdef: path.to_string(), app_data: None, guards: Vec::new(), services: Vec::new(), default: None, external: Vec::new(), factory_ref, } } } impl Scope where T: ServiceFactory, { /// Add match guard to a scope. /// /// ``` /// use actix_web::{web, guard, App, HttpRequest, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// /// let app = App::new().service( /// web::scope("/app") /// .guard(guard::Header("content-type", "text/plain")) /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|r: HttpRequest| { /// HttpResponse::MethodNotAllowed() /// })) /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); self } /// Add scope data. /// /// Data of different types from parent contexts will still be accessible. Any `Data` types /// set here can be extracted in handlers using the `Data` extractor. /// /// # Examples /// ``` /// use std::cell::Cell; /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { /// count: std::cell::Cell, /// } /// /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { /// // note this cannot use the Data extractor because it was not added with it /// let incr = *req.app_data::().unwrap(); /// assert_eq!(incr, 3); /// /// // update counter using other value from app data /// counter.count.set(counter.count.get() + incr); /// /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// /// let app = App::new().service( /// web::scope("/app") /// .app_data(3usize) /// .app_data(web::Data::new(MyData { count: Default::default() })) /// .route("/", web::get().to(handler)) /// ); /// ``` #[doc(alias = "manage")] pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) .insert(data); self } /// Add scope data after wrapping in `Data`. /// /// Deprecated in favor of [`app_data`](Self::app_data). #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } /// Run external configuration as part of the scope building process. /// /// This function is useful for moving parts of configuration to a different module or library. /// For example, some of the resource's configuration could be moved to different module. /// /// ``` /// use actix_web::{web, middleware, App, HttpResponse}; /// /// // this function could be located in different module /// fn config(cfg: &mut web::ServiceConfig) { /// cfg.service(web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// /// let app = App::new() /// .wrap(middleware::Logger::default()) /// .service( /// web::scope("/api") /// .configure(config) /// ) /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, cfg_fn: F) -> Self where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); cfg_fn(&mut cfg); self.services.extend(cfg.services); self.external.extend(cfg.external); // TODO: add Extensions::is_empty check and conditionally insert data self.app_data .get_or_insert_with(Extensions::new) .extend(cfg.app_data); if let Some(default) = cfg.default { self.default = Some(default); } self } /// Register HTTP service. /// /// This is similar to `App's` service registration. /// /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. /// /// ``` /// use actix_web::{web, App, HttpRequest}; /// /// struct AppState; /// /// async fn index(req: HttpRequest) -> &'static str { /// "Welcome!" /// } /// /// let app = App::new().service( /// web::scope("/app").service( /// web::scope("/v1") /// .service(web::resource("/test1").to(index))) /// ); /// ``` pub fn service(mut self, factory: F) -> Self where F: HttpServiceFactory + 'static, { self.services .push(Box::new(ServiceFactoryWrapper::new(factory))); self } /// Configure route for a specific path. /// /// This is a simplified version of the `Scope::service()` method. /// This method can be called multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// /// let app = App::new().service( /// web::scope("/app") /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( Resource::new(path) .add_guards(route.take_guards()) .route(route), ) } /// Default service to be used if no matching resource could be found. /// /// If a default service is not registered, it will fall back to the default service of /// the parent [`App`](crate::App) (see [`App::default_service`](crate::App::default_service)). pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, U: ServiceFactory + 'static, U::InitError: fmt::Debug, { // create and configure default resource self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err( |e| log::error!("Can not construct default service: {:?}", e), )))); self } /// Registers a scope-wide middleware. /// /// `mw` is a middleware component (type), that can modify the request and response across all /// sub-resources managed by this `Scope`. /// /// See [`App::wrap`](crate::App::wrap) for more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap( self, mw: M, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where M: Transform< T::Service, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody, { Scope { endpoint: apply(mw, self.endpoint), rdef: self.rdef, app_data: self.app_data, guards: self.guards, services: self.services, default: self.default, external: self.external, factory_ref: self.factory_ref, } } /// Registers a scope-wide function middleware. /// /// `mw` is a closure that runs during inbound and/or outbound processing in the request /// life-cycle (request -> response), modifying request/response as necessary, across all /// requests handled by the `Scope`. /// /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap_fn( self, mw: F, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, R: Future, Error>>, B: MessageBody, { Scope { endpoint: apply_fn_factory(self.endpoint, mw), rdef: self.rdef, app_data: self.app_data, guards: self.guards, services: self.services, default: self.default, external: self.external, factory_ref: self.factory_ref, } } } impl HttpServiceFactory for Scope where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { // update default resource if needed let default = self.default.unwrap_or_else(|| config.default_service()); // register nested services let mut cfg = config.clone_config(); self.services .into_iter() .for_each(|mut srv| srv.register(&mut cfg)); let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); // external resources for mut rdef in mem::take(&mut self.external) { rmap.add(&mut rdef, None); } // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { default, services: cfg .into_services() .1 .into_iter() .map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested); (rdef, srv, RefCell::new(guards)) }) .collect::>() .into_boxed_slice() .into(), }); // get guards let guards = if self.guards.is_empty() { None } else { Some(self.guards) }; let scope_data = self.app_data.map(Rc::new); // wraps endpoint service (including middleware) call and injects app data for this scope let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { if let Some(ref data) = scope_data { req.add_data_container(Rc::clone(data)); } let fut = srv.call(req); async { Ok(fut.await?.map_into_boxed_body()) } }); // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, endpoint, Some(Rc::new(rmap)), ) } } pub struct ScopeFactory { #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, BoxedHttpServiceFactory, RefCell>, )], >, default: Rc, } impl ServiceFactory for ScopeFactory { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = ScopeService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { // construct default service factory future let default_fut = self.default.new_service(()); // construct all services factory future with it's resource def and guards. let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { let path = path.clone(); let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { factory_fut .await .map(move |service| (path, guards, service)) } })); Box::pin(async move { let default = default_fut.await?; // build router from the factory future result. let router = factory_fut .await .into_iter() .collect::, _>>()? .drain(..) .fold(Router::build(), |mut router, (path, guards, service)| { router.push(path, service, guards); router }) .finish(); Ok(ScopeService { router, default }) }) } } pub struct ScopeService { router: Router>>, default: BoxedHttpService, } impl Service for ScopeService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { let guard_ctx = req.guard_ctx(); guards.iter().all(|guard| guard.check(&guard_ctx)) }); if let Some((srv, _info)) = res { srv.call(req) } else { self.default.call(req) } } } #[doc(hidden)] pub struct ScopeEndpoint { factory: Rc>>, } impl ScopeEndpoint { fn new(factory: Rc>>) -> Self { ScopeEndpoint { factory } } } impl ServiceFactory for ScopeEndpoint { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = ScopeService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(()) } } #[cfg(test)] mod tests { use actix_utils::future::ok; use bytes::Bytes; use super::*; use crate::{ guard, http::{ header::{self, HeaderValue}, Method, StatusCode, }, middleware::DefaultHeaders, test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, }; #[test] fn can_be_returned_from_fn() { fn my_scope_1() -> Scope { web::scope("/test") .service(web::resource("").route(web::get().to(|| async { "hello" }))) } fn my_scope_2() -> Scope< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), >, > { web::scope("/test-compat") .wrap_fn(|req, srv| { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) .service(web::resource("").route(web::get().to(|| async { "hello" }))) } fn my_scope_3() -> impl HttpServiceFactory { my_scope_2() } App::new() .service(my_scope_1()) .service(my_scope_2()) .service(my_scope_3()); } #[actix_rt::test] async fn test_scope() { let srv = init_service( App::new() .service(web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok))), ) .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_root() { let srv = init_service( App::new().service( web::scope("/app") .service(web::resource("").to(HttpResponse::Ok)) .service(web::resource("/").to(HttpResponse::Created)), ), ) .await; let req = TestRequest::with_uri("/app").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[actix_rt::test] async fn test_scope_root2() { let srv = init_service( App::new().service(web::scope("/app/").service(web::resource("").to(HttpResponse::Ok))), ) .await; let req = TestRequest::with_uri("/app").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_root3() { let srv = init_service( App::new() .service(web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok))), ) .await; let req = TestRequest::with_uri("/app").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_scope_route() { let srv = init_service( App::new().service( web::scope("app") .route("/path1", web::get().to(HttpResponse::Ok)) .route("/path1", web::delete().to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_scope_route_without_leading_slash() { let srv = init_service( App::new().service( web::scope("app").service( web::resource("path1") .route(web::get().to(HttpResponse::Ok)) .route(web::delete().to(HttpResponse::Ok)), ), ), ) .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[actix_rt::test] async fn test_scope_guard() { let srv = init_service( App::new().service( web::scope("/app") .guard(guard::Get()) .service(web::resource("/path1").to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/path1") .method(Method::GET) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_variable_segment() { let srv = init_service(App::new().service(web::scope("/ab-{project}").service( web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Ok().body(format!("project: {}", &r.match_info()["project"])) }), ))) .await; let req = TestRequest::with_uri("/ab-project1/path1").to_request(); let res = srv.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_body_eq!(res, b"project: project1"); let req = TestRequest::with_uri("/aa-project1/path1").to_request(); let res = srv.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_nested_scope() { let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/t1").service(web::resource("/path1").to(HttpResponse::Created)), ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[actix_rt::test] async fn test_nested_scope_no_slash() { let srv = init_service(App::new().service(web::scope("/app").service( web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[actix_rt::test] async fn test_nested_scope_root() { let srv = init_service( App::new().service( web::scope("/app").service( web::scope("/t1") .service(web::resource("").to(HttpResponse::Ok)) .service(web::resource("/").to(HttpResponse::Created)), ), ), ) .await; let req = TestRequest::with_uri("/app/t1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/t1/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[actix_rt::test] async fn test_nested_scope_filter() { let srv = init_service( App::new().service( web::scope("/app").service( web::scope("/t1") .guard(guard::Get()) .service(web::resource("/path1").to(HttpResponse::Ok)), ), ), ) .await; let req = TestRequest::with_uri("/app/t1/path1") .method(Method::POST) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::GET) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_nested_scope_with_variable_segment() { let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project_id}").service(web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Created().body(format!("project: {}", &r.match_info()["project_id"])) })), ))) .await; let req = TestRequest::with_uri("/app/project_1/path1").to_request(); let res = srv.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::CREATED); assert_body_eq!(res, b"project: project_1"); } #[actix_rt::test] async fn test_nested2_scope_with_variable_segment() { let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project}").service(web::scope("/{id}").service( web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Created().body(format!( "project: {} - {}", &r.match_info()["project"], &r.match_info()["id"], )) }), )), ))) .await; let req = TestRequest::with_uri("/app/test/1/path1").to_request(); let res = srv.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::CREATED); assert_body_eq!(res, b"project: test - 1"); let req = TestRequest::with_uri("/app/test/1/path2").to_request(); let res = srv.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_resource() { let srv = init_service( App::new().service( web::scope("/app") .service(web::resource("/path1").to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), ), ) .await; let req = TestRequest::with_uri("/app/path2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/path2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_resource_propagation() { let srv = init_service( App::new() .service(web::scope("/app1").default_service(web::to(HttpResponse::BadRequest))) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) }), ) .await; let req = TestRequest::with_uri("/non-exist").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/app1/non-exist").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/app2/non-exist").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[actix_rt::test] async fn test_middleware() { let srv = init_service( App::new().service( web::scope("app") .wrap( DefaultHeaders::new() .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_middleware_body_type() { // Compile test that Scope accepts any body type; test for `EitherBody` let srv = init_service( App::new().service( web::scope("app") .wrap_fn(|req, srv| { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) .service(web::resource("/test").route(web::get().to(|| async { "hello" }))), ), ) .await; // test if `MessageBody::try_into_bytes()` is preserved across scope layer use actix_http::body::MessageBody as _; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; let body = resp.into_body(); assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); } #[actix_rt::test] async fn test_middleware_fn() { let srv = init_service( App::new().service( web::scope("app") .wrap_fn(|req, srv| { let fut = srv.call(req); async move { let mut res = fut.await?; res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) .route("/test", web::get().to(HttpResponse::Ok)), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } #[actix_rt::test] async fn test_middleware_app_data() { let srv = init_service( App::new().service( web::scope("app") .app_data(1usize) .wrap_fn(|req, srv| { assert_eq!(req.app_data::(), Some(&1usize)); req.extensions_mut().insert(1usize); srv.call(req) }) .route("/test", web::get().to(HttpResponse::Ok)) .default_service(|req: ServiceRequest| async move { let (req, _) = req.into_parts(); assert_eq!(req.extensions().get::(), Some(&1)); Ok(ServiceResponse::new( req, HttpResponse::BadRequest().finish(), )) }), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/default").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } // allow deprecated {App, Scope}::data #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = init_service(App::new().data(1usize).service( web::scope("app").data(10usize).route( "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() }), ), )) .await; let req = TestRequest::with_uri("/app/t").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } // allow deprecated `{App, Scope}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { let srv = init_service(App::new().data(1usize).service( web::scope("app").data(10usize).default_service(web::to( |data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() }, )), )) .await; let req = TestRequest::with_uri("/app/t").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_override_app_data() { let srv = init_service(App::new().app_data(web::Data::new(1usize)).service( web::scope("app").app_data(web::Data::new(10usize)).route( "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() }), ), )) .await; let req = TestRequest::with_uri("/app/t").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_config() { let srv = init_service(App::new().service(web::scope("/app").configure(|s| { s.route("/path1", web::get().to(HttpResponse::Ok)); }))) .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_config_2() { let srv = init_service(App::new().service(web::scope("/app").configure(|s| { s.service(web::scope("/v1").configure(|s| { s.route("/", web::get().to(HttpResponse::Ok)); })); }))) .await; let req = TestRequest::with_uri("/app/v1/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_url_for_external() { let srv = init_service(App::new().service(web::scope("/app").configure(|s| { s.service(web::scope("/v1").configure(|s| { s.external_resource("youtube", "https://youtube.com/watch/{video_id}"); s.route( "/", web::get().to(|req: HttpRequest| { HttpResponse::Ok() .body(req.url_for("youtube", ["xxxxxx"]).unwrap().to_string()) }), ); })); }))) .await; let req = TestRequest::with_uri("/app/v1/").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, &b"https://youtube.com/watch/xxxxxx"[..]); } #[actix_rt::test] async fn test_url_for_nested() { let srv = init_service(App::new().service(web::scope("/a").service( web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(web::get().to( |req: HttpRequest| { HttpResponse::Ok().body(format!("{}", req.url_for("c", ["12345"]).unwrap())) }, ))), ))) .await; let req = TestRequest::with_uri("/a/b/c/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!( body, Bytes::from_static(b"http://localhost:8080/a/b/c/12345") ); } #[actix_rt::test] async fn dynamic_scopes() { let srv = init_service( App::new().service( web::scope("/{a}/").service( web::scope("/{b}/") .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) .route( "/", web::get().to(|_: HttpRequest| HttpResponse::Accepted()), ) .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), ), ), ) .await; // note the unintuitive behavior with trailing slashes on scopes with dynamic segments let req = TestRequest::with_uri("/a//b//c").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/a//b/").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/a//b//").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::ACCEPTED); let req = TestRequest::with_uri("/a//b//c/d").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); let srv = init_service( App::new().service( web::scope("/{a}").service( web::scope("/{b}") .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) .route( "/", web::get().to(|_: HttpRequest| HttpResponse::Accepted()), ) .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), ), ), ) .await; let req = TestRequest::with_uri("/a/b/c").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/a/b").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/a/b/").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::ACCEPTED); let req = TestRequest::with_uri("/a/b/c/d").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } } actix-web-4.9.0/src/server.rs000064400000000000000000001201071046102023000141650ustar 00000000000000use std::{ any::Any, cmp, fmt, io, marker::PhantomData, net, sync::{Arc, Mutex}, time::Duration, }; #[cfg(feature = "__tls")] use actix_http::TlsAcceptorConfig; use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; use actix_server::{Server, ServerBuilder}; use actix_service::{ map_config, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; use crate::{config::AppConfig, Error}; struct Socket { scheme: &'static str, addr: net::SocketAddr, } struct Config { host: Option, keep_alive: KeepAlive, client_request_timeout: Duration, client_disconnect_timeout: Duration, #[allow(dead_code)] // only dead when no TLS features are enabled tls_handshake_timeout: Option, } /// An HTTP Server. /// /// Create new HTTP server with application factory. /// /// # Automatic HTTP Version Selection /// /// There are two ways to select the HTTP version of an incoming connection: /// /// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both /// versions are supported automatically when using either of the `.bind_rustls()` or /// `.bind_openssl()` methods. /// - The other is to read the first few bytes of the TCP stream. This is the only viable approach /// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use /// the `.bind_auto_h2c()` method to enable this behavior. /// /// # Examples /// /// ```no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// #[actix_web::main] /// async fn main() -> std::io::Result<()> { /// HttpServer::new(|| { /// App::new() /// .service(web::resource("/").to(|| async { "hello world" })) /// }) /// .bind(("127.0.0.1", 8080))? /// .run() /// .await /// } /// ``` pub struct HttpServer where F: Fn() -> I + Send + Clone + 'static, I: IntoServiceFactory, S: ServiceFactory, S::Error: Into, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, { pub(super) factory: F, config: Arc>, backlog: u32, sockets: Vec, builder: ServerBuilder, #[allow(clippy::type_complexity)] on_connect_fn: Option>, _phantom: PhantomData<(S, B)>, } impl HttpServer where F: Fn() -> I + Send + Clone + 'static, I: IntoServiceFactory, S: ServiceFactory + 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, S::Service: 'static, B: MessageBody + 'static, { /// Create new HTTP server with application factory /// /// # Worker Count /// /// The `factory` will be instantiated multiple times in most configurations. See /// [`bind()`](Self::bind()) docs for more on how worker count and bind address resolution /// causes multiple server factory instantiations. pub fn new(factory: F) -> Self { HttpServer { factory, config: Arc::new(Mutex::new(Config { host: None, keep_alive: KeepAlive::default(), client_request_timeout: Duration::from_secs(5), client_disconnect_timeout: Duration::from_secs(1), tls_handshake_timeout: None, })), backlog: 1024, sockets: Vec::new(), builder: ServerBuilder::default(), on_connect_fn: None, _phantom: PhantomData, } } /// Sets number of workers to start (per bind address). /// /// The default worker count is the determined by [`std::thread::available_parallelism()`]. See /// its documentation to determine what behavior you should expect when server is run. /// /// Note that the server factory passed to [`new`](Self::new()) will be instantiated **at least /// once per worker**. See [`bind()`](Self::bind()) docs for more on how worker count and bind /// address resolution causes multiple server factory instantiations. /// /// `num` must be greater than 0. /// /// # Panics /// /// Panics if `num` is 0. pub fn workers(mut self, num: usize) -> Self { self.builder = self.builder.workers(num); self } /// Sets server keep-alive preference. /// /// By default keep-alive is set to 5 seconds. pub fn keep_alive>(self, val: T) -> Self { self.config.lock().unwrap().keep_alive = val.into(); self } /// Sets the maximum number of pending connections. /// /// This refers to the number of clients that can be waiting to be served. Exceeding this number /// results in the client getting an error when attempting to connect. It should only affect /// servers under significant load. /// /// Generally set in the 64–2048 range. Default value is 2048. /// /// This method will have no effect if called after a `bind()`. pub fn backlog(mut self, backlog: u32) -> Self { self.backlog = backlog; self.builder = self.builder.backlog(backlog); self } /// Sets the per-worker maximum number of concurrent connections. /// /// All socket listeners will stop accepting connections when this limit is reached for /// each worker. /// /// By default max connections is set to a 25k. pub fn max_connections(mut self, num: usize) -> Self { self.builder = self.builder.max_concurrent_connections(num); self } /// Sets the per-worker maximum concurrent TLS connection limit. /// /// All listeners will stop accepting connections when this limit is reached. It can be used to /// limit the global TLS CPU usage. /// /// By default max connections is set to a 256. #[allow(unused_variables)] pub fn max_connection_rate(self, num: usize) -> Self { #[cfg(feature = "__tls")] actix_tls::accept::max_concurrent_tls_connect(num); self } /// Sets max number of threads for each worker's blocking task thread pool. /// /// One thread pool is set up **per worker**; not shared across workers. /// /// By default set to 512 divided by the number of workers. pub fn worker_max_blocking_threads(mut self, num: usize) -> Self { self.builder = self.builder.worker_max_blocking_threads(num); self } /// Sets server client timeout for first request. /// /// Defines a timeout for reading client request head. If a client does not transmit the entire /// set headers within this time, the request is terminated with a 408 (Request Timeout) error. /// /// To disable timeout set value to 0. /// /// By default client timeout is set to 5000 milliseconds. pub fn client_request_timeout(self, dur: Duration) -> Self { self.config.lock().unwrap().client_request_timeout = dur; self } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `client_request_timeout`.")] pub fn client_timeout(self, dur: Duration) -> Self { self.client_request_timeout(dur) } /// Sets server connection shutdown timeout. /// /// Defines a timeout for connection shutdown. If a shutdown procedure does not complete within /// this time, the request is dropped. /// /// To disable timeout set value to 0. /// /// By default client timeout is set to 5000 milliseconds. pub fn client_disconnect_timeout(self, dur: Duration) -> Self { self.config.lock().unwrap().client_disconnect_timeout = dur; self } /// Sets TLS handshake timeout. /// /// Defines a timeout for TLS handshake. If the TLS handshake does not complete within this /// time, the connection is closed. /// /// By default, the handshake timeout is 3 seconds. #[cfg(feature = "__tls")] pub fn tls_handshake_timeout(self, dur: Duration) -> Self { self.config .lock() .unwrap() .tls_handshake_timeout .replace(dur); self } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")] pub fn client_shutdown(self, dur: u64) -> Self { self.client_disconnect_timeout(Duration::from_millis(dur)) } /// Sets function that will be called once before each connection is handled. /// /// It will receive a `&std::any::Any`, which contains underlying connection type and an /// [Extensions] container so that connection data can be accessed in middleware and handlers. /// /// # Connection Types /// - `actix_tls::accept::openssl::TlsStream` when using OpenSSL. /// - `actix_tls::accept::rustls_0_20::TlsStream` when using /// Rustls v0.20. /// - `actix_tls::accept::rustls_0_21::TlsStream` when using /// Rustls v0.21. /// - `actix_tls::accept::rustls_0_22::TlsStream` when using /// Rustls v0.22. /// - `actix_tls::accept::rustls_0_23::TlsStream` when using /// Rustls v0.23. /// - `actix_web::rt::net::TcpStream` when no encryption is used. /// /// See the `on_connect` example for additional details. pub fn on_connect(self, f: CB) -> HttpServer where CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static, { HttpServer { factory: self.factory, config: self.config, backlog: self.backlog, sockets: self.sockets, builder: self.builder, on_connect_fn: Some(Arc::new(f)), _phantom: PhantomData, } } /// Sets server host name. /// /// Host name is used by application router as a hostname for url generation. Check /// [`ConnectionInfo`](crate::dev::ConnectionInfo::host()) docs for more info. /// /// By default, hostname is set to "localhost". pub fn server_hostname>(self, val: T) -> Self { self.config.lock().unwrap().host = Some(val.as_ref().to_owned()); self } /// Flags the `System` to exit after server shutdown. /// /// Does nothing when running under `#[tokio::main]` runtime. pub fn system_exit(mut self) -> Self { self.builder = self.builder.system_exit(); self } /// Disables signal handling. pub fn disable_signals(mut self) -> Self { self.builder = self.builder.disable_signals(); self } /// Sets timeout for graceful worker shutdown of workers. /// /// After receiving a stop signal, workers have this much time to finish serving requests. /// Workers still alive after the timeout are force dropped. /// /// By default shutdown timeout sets to 30 seconds. pub fn shutdown_timeout(mut self, sec: u64) -> Self { self.builder = self.builder.shutdown_timeout(sec); self } /// Returns addresses of bound sockets. pub fn addrs(&self) -> Vec { self.sockets.iter().map(|s| s.addr).collect() } /// Returns addresses of bound sockets and the scheme for it. /// /// This is useful when the server is bound from different sources with some sockets listening /// on HTTP and some listening on HTTPS and the user should be presented with an enumeration of /// which socket requires which protocol. pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() } /// Resolves socket address(es) and binds server to created listener(s). /// /// # Hostname Resolution /// /// When `addrs` includes a hostname, it is possible for this method to bind to both the IPv4 /// and IPv6 addresses that result from a DNS lookup. You can test this by passing /// `localhost:8080` and noting that the server binds to `127.0.0.1:8080` _and_ `[::1]:8080`. To /// bind additional addresses, call this method multiple times. /// /// Note that, if a DNS lookup is required, resolving hostnames is a blocking operation. /// /// # Worker Count /// /// The `factory` will be instantiated multiple times in most scenarios. The number of /// instantiations is number of [`workers`](Self::workers()) × number of sockets resolved by /// `addrs`. /// /// For example, if you've manually set [`workers`](Self::workers()) to 2, and use `127.0.0.1` /// as the bind `addrs`, then `factory` will be instantiated twice. However, using `localhost` /// as the bind `addrs` can often resolve to both `127.0.0.1` (IPv4) _and_ `::1` (IPv6), causing /// the `factory` to be instantiated 4 times (2 workers × 2 bind addresses). /// /// Using a bind address of `0.0.0.0`, which signals to use all interfaces, may also multiple /// the number of instantiations in a similar way. /// /// # Typical Usage /// /// In general, use `127.0.0.1:` when testing locally and `0.0.0.0:` when deploying /// (with or without a reverse proxy or load balancer) so that the server is accessible. /// /// # Errors /// /// Returns an `io::Error` if: /// - `addrs` cannot be resolved into one or more socket addresses; /// - all the resolved socket addresses are already bound. /// /// # Example /// /// ``` /// # use actix_web::{App, HttpServer}; /// # fn inner() -> std::io::Result<()> { /// HttpServer::new(|| App::new()) /// .bind(("127.0.0.1", 8080))? /// .bind("[::1]:9000")? /// # ; Ok(()) } /// ``` pub fn bind(mut self, addrs: A) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen(lst)?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for plaintext HTTP/1.x /// or HTTP/2 connections. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. #[cfg(feature = "http2")] pub fn bind_auto_h2c(mut self, addrs: A) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen_auto_h2c(lst)?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using Rustls v0.20. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_20")] pub fn bind_rustls( mut self, addrs: A, config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen_rustls_0_20_inner(lst, config.clone())?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using Rustls v0.21. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_21")] pub fn bind_rustls_021( mut self, addrs: A, config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, ) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen_rustls_0_21_inner(lst, config.clone())?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using Rustls v0.22. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_22")] pub fn bind_rustls_0_22( mut self, addrs: A, config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, ) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen_rustls_0_22_inner(lst, config.clone())?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using Rustls v0.23. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_23")] pub fn bind_rustls_0_23( mut self, addrs: A, config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, ) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen_rustls_0_23_inner(lst, config.clone())?; } Ok(self) } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using OpenSSL. /// /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "openssl")] pub fn bind_openssl(mut self, addrs: A, builder: SslAcceptorBuilder) -> io::Result where A: net::ToSocketAddrs, { let sockets = bind_addrs(addrs, self.backlog)?; let acceptor = openssl_acceptor(builder)?; for lst in sockets { self = self.listen_openssl_inner(lst, acceptor.clone())?; } Ok(self) } /// Binds to existing listener for accepting incoming connection requests. /// /// No changes are made to `lst`'s configuration. Ensure it is configured properly before /// passing ownership to `listen()`. pub fn listen(mut self, lst: net::TcpListener) -> io::Result { let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "http", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let cfg = cfg.lock().unwrap(); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let mut svc = HttpService::build() .keep_alive(cfg.keep_alive) .client_request_timeout(cfg.client_request_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { svc = svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); svc.finish(map_config(fac, move |_| { AppConfig::new(false, host.clone(), addr) })) .tcp() })?; Ok(self) } /// Binds to existing listener for accepting incoming plaintext HTTP/1.x or HTTP/2 connections. #[cfg(feature = "http2")] pub fn listen_auto_h2c(mut self, lst: net::TcpListener) -> io::Result { let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "http", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let cfg = cfg.lock().unwrap(); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let mut svc = HttpService::build() .keep_alive(cfg.keep_alive) .client_request_timeout(cfg.client_request_timeout) .client_disconnect_timeout(cfg.client_disconnect_timeout) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { svc = svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); svc.finish(map_config(fac, move |_| { AppConfig::new(false, host.clone(), addr) })) .tcp_auto_h2c() })?; Ok(self) } /// Binds to existing listener for accepting incoming TLS connection requests using Rustls /// v0.20. /// /// See [`listen()`](Self::listen) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_20")] pub fn listen_rustls( self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { self.listen_rustls_0_20_inner(lst, config) } /// Binds to existing listener for accepting incoming TLS connection requests using Rustls /// v0.21. /// /// See [`listen()`](Self::listen()) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_21")] pub fn listen_rustls_0_21( self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, ) -> io::Result { self.listen_rustls_0_21_inner(lst, config) } #[cfg(feature = "rustls-0_20")] fn listen_rustls_0_20_inner( mut self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "https", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), }; svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .rustls_with_config(config.clone(), acceptor_config) })?; Ok(self) } #[cfg(feature = "rustls-0_21")] fn listen_rustls_0_21_inner( mut self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "https", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), }; svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .rustls_021_with_config(config.clone(), acceptor_config) })?; Ok(self) } /// Binds to existing listener for accepting incoming TLS connection requests using Rustls /// v0.22. /// /// See [`listen()`](Self::listen()) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_22")] pub fn listen_rustls_0_22( self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, ) -> io::Result { self.listen_rustls_0_22_inner(lst, config) } #[cfg(feature = "rustls-0_22")] fn listen_rustls_0_22_inner( mut self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "https", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), }; svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .rustls_0_22_with_config(config.clone(), acceptor_config) })?; Ok(self) } /// Binds to existing listener for accepting incoming TLS connection requests using Rustls /// v0.23. /// /// See [`listen()`](Self::listen()) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "rustls-0_23")] pub fn listen_rustls_0_23( self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, ) -> io::Result { self.listen_rustls_0_23_inner(lst, config) } #[cfg(feature = "rustls-0_23")] fn listen_rustls_0_23_inner( mut self, lst: net::TcpListener, config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "https", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), }; svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .rustls_0_23_with_config(config.clone(), acceptor_config) })?; Ok(self) } /// Binds to existing listener for accepting incoming TLS connection requests using OpenSSL. /// /// See [`listen()`](Self::listen) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "openssl")] pub fn listen_openssl( self, lst: net::TcpListener, builder: SslAcceptorBuilder, ) -> io::Result { self.listen_openssl_inner(lst, openssl_acceptor(builder)?) } #[cfg(feature = "openssl")] fn listen_openssl_inner( mut self, lst: net::TcpListener, acceptor: SslAcceptor, ) -> io::Result { let factory = self.factory.clone(); let cfg = Arc::clone(&self.config); let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, scheme: "https", }); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout) .local_addr(addr); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); // false positive lint (?) #[allow(clippy::significant_drop_in_scrutinee)] let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), }; svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .openssl_with_config(acceptor.clone(), acceptor_config) })?; Ok(self) } /// Opens Unix Domain Socket (UDS) from `uds` path and binds server to created listener. #[cfg(unix)] pub fn bind_uds(mut self, uds_path: A) -> io::Result where A: AsRef, { use actix_http::Protocol; use actix_rt::net::UnixStream; use actix_service::{fn_service, ServiceFactoryExt as _}; let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let socket_addr = net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); self.sockets.push(Socket { scheme: "http", addr: socket_addr, }); self.builder = self.builder.bind_uds( format!("actix-web-service-{:?}", uds_path.as_ref()), uds_path, move || { let c = cfg.lock().unwrap(); let config = AppConfig::new( false, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), socket_addr, ); let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then( HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout) .finish(map_config(fac, move |_| config.clone())), ) }, )?; Ok(self) } /// Binds to existing Unix Domain Socket (UDS) listener. #[cfg(unix)] pub fn listen_uds(mut self, lst: std::os::unix::net::UnixListener) -> io::Result { use actix_http::Protocol; use actix_rt::net::UnixStream; use actix_service::{fn_service, ServiceFactoryExt as _}; let cfg = Arc::clone(&self.config); let factory = self.factory.clone(); let socket_addr = net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); self.sockets.push(Socket { scheme: "http", addr: socket_addr, }); let addr = lst.local_addr()?; let name = format!("actix-web-service-{:?}", addr); let on_connect_fn = self.on_connect_fn.clone(); self.builder = self.builder.listen_uds(name, lst, move || { let c = cfg.lock().unwrap(); let config = AppConfig::new( false, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), socket_addr, ); fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ let mut svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout); if let Some(handler) = on_connect_fn.clone() { svc = svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); } let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); svc.finish(map_config(fac, move |_| config.clone())) }) })?; Ok(self) } } impl HttpServer where F: Fn() -> I + Send + Clone + 'static, I: IntoServiceFactory, S: ServiceFactory, S::Error: Into, S::InitError: fmt::Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, { /// Start listening for incoming connections. /// /// # Workers /// This method starts a number of HTTP workers in separate threads. The number of workers in a /// set is defined by [`workers()`](Self::workers) or, by default, the number of the machine's /// physical cores. One worker set is created for each socket address to be bound. For example, /// if workers is set to 4, and there are 2 addresses to bind, then 8 worker threads will be /// spawned. /// /// # Panics /// This methods panics if no socket addresses were successfully bound or if no Tokio runtime /// is set up. pub fn run(self) -> Server { self.builder.run() } } /// Bind TCP listeners to socket addresses resolved from `addrs` with options. fn bind_addrs(addrs: impl net::ToSocketAddrs, backlog: u32) -> io::Result> { let mut err = None; let mut success = false; let mut sockets = Vec::new(); for addr in addrs.to_socket_addrs()? { match create_tcp_listener(addr, backlog) { Ok(lst) => { success = true; sockets.push(lst); } Err(error) => err = Some(error), } } if success { Ok(sockets) } else if let Some(err) = err.take() { Err(err) } else { Err(io::Error::new( io::ErrorKind::Other, "Can not bind to address.", )) } } /// Creates a TCP listener from socket address and options. fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; let domain = Domain::for_address(addr); let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; socket.set_reuse_address(true)?; socket.bind(&addr.into())?; // clamp backlog to max u32 that fits in i32 range let backlog = cmp::min(backlog, i32::MAX as u32) as i32; socket.listen(backlog)?; Ok(net::TcpListener::from(socket)) } /// Configures OpenSSL acceptor `builder` with ALPN protocols. #[cfg(feature = "openssl")] fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; const H11: &[u8] = b"\x08http/1.1"; if protocols.windows(3).any(|window| window == H2) { Ok(b"h2") } else if protocols.windows(9).any(|window| window == H11) { Ok(b"http/1.1") } else { Err(AlpnError::NOACK) } }); builder.set_alpn_protos(b"\x08http/1.1\x02h2")?; Ok(builder.build()) } actix-web-4.9.0/src/service.rs000064400000000000000000000611461046102023000143260ustar 00000000000000use std::{ cell::{Ref, RefMut}, fmt, net, rc::Rc, }; use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response, ResponseHead, StatusCode, Uri, Version, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; use actix_service::{ boxed::{BoxService, BoxServiceFactory}, IntoServiceFactory, ServiceFactory, }; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, dev::ensure_leading_slash, guard::{Guard, GuardContext}, info::ConnectionInfo, rmap::ResourceMap, Error, FromRequest, HttpRequest, HttpResponse, }; pub(crate) type BoxedHttpService = BoxService, Error>; pub(crate) type BoxedHttpServiceFactory = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; pub trait HttpServiceFactory { fn register(self, config: &mut AppService); } impl HttpServiceFactory for Vec { fn register(self, config: &mut AppService) { self.into_iter() .for_each(|factory| factory.register(config)); } } pub(crate) trait AppServiceFactory { fn register(&mut self, config: &mut AppService); } pub(crate) struct ServiceFactoryWrapper { factory: Option, } impl ServiceFactoryWrapper { pub fn new(factory: T) -> Self { Self { factory: Some(factory), } } } impl AppServiceFactory for ServiceFactoryWrapper where T: HttpServiceFactory, { fn register(&mut self, config: &mut AppService) { if let Some(item) = self.factory.take() { item.register(config) } } } /// A service level request wrapper. /// /// Allows mutable access to request's internal structures. pub struct ServiceRequest { req: HttpRequest, payload: Payload, } impl ServiceRequest { /// Construct `ServiceRequest` from parts. pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } } /// Deconstruct `ServiceRequest` into inner parts. #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } /// Returns mutable accessors to inner parts. #[inline] pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { (&mut self.req, &mut self.payload) } /// Returns immutable accessors to inner parts. #[inline] pub fn parts(&self) -> (&HttpRequest, &Payload) { (&self.req, &self.payload) } /// Returns immutable accessor to inner [`HttpRequest`]. #[inline] pub fn request(&self) -> &HttpRequest { &self.req } /// Derives a type from this request using an [extractor](crate::FromRequest). /// /// Returns the `T` extractor's `Future` type which can be `await`ed. This is particularly handy /// when you want to use an extractor in a middleware implementation. /// /// # Examples /// ``` /// use actix_web::{ /// dev::{ServiceRequest, ServiceResponse}, /// web::Path, Error /// }; /// /// async fn my_helper(mut srv_req: ServiceRequest) -> Result { /// let path = srv_req.extract::>().await?; /// // [...] /// # todo!() /// } /// ``` pub fn extract(&mut self) -> ::Future where T: FromRequest, { T::from_request(&self.req, &mut self.payload) } /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { #[cfg(debug_assertions)] if Rc::strong_count(&req.inner) > 1 { log::warn!("Cloning an `HttpRequest` might cause panics."); } Self { req, payload } } /// Construct `ServiceRequest` with no payload from given `HttpRequest`. #[inline] pub fn from_request(req: HttpRequest) -> Self { ServiceRequest { req, payload: Payload::None, } } /// Create `ServiceResponse` from this request and given response. #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { let res = HttpResponse::from(res.into()); ServiceResponse::new(self.req, res) } /// Create `ServiceResponse` from this request and given error. #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { let res = HttpResponse::from_error(err.into()); ServiceResponse::new(self.req, res) } /// Returns a reference to the request head. #[inline] pub fn head(&self) -> &RequestHead { self.req.head() } /// Returns a mutable reference to the request head. #[inline] pub fn head_mut(&mut self) -> &mut RequestHead { self.req.head_mut() } /// Returns the request URI. #[inline] pub fn uri(&self) -> &Uri { &self.head().uri } /// Returns the request method. #[inline] pub fn method(&self) -> &Method { &self.head().method } /// Returns the request version. #[inline] pub fn version(&self) -> Version { self.head().version } /// Returns a reference to request headers. #[inline] pub fn headers(&self) -> &HeaderMap { &self.head().headers } /// Returns a mutable reference to request headers. #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } /// Returns request path. #[inline] pub fn path(&self) -> &str { self.head().uri.path() } /// Counterpart to [`HttpRequest::query_string`]. #[inline] pub fn query_string(&self) -> &str { self.req.query_string() } /// Returns peer's socket address. /// /// See [`HttpRequest::peer_addr`] for more details. /// /// [`HttpRequest::peer_addr`]: crate::HttpRequest::peer_addr #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr } /// Returns a reference to connection info. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { self.req.connection_info() } /// Counterpart to [`HttpRequest::match_info`]. #[inline] pub fn match_info(&self) -> &Path { self.req.match_info() } /// Returns a mutable reference to the path match information. #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() } /// Counterpart to [`HttpRequest::match_name`]. #[inline] pub fn match_name(&self) -> Option<&str> { self.req.match_name() } /// Counterpart to [`HttpRequest::match_pattern`]. #[inline] pub fn match_pattern(&self) -> Option { self.req.match_pattern() } /// Returns a reference to the application's resource map. /// Counterpart to [`HttpRequest::resource_map`]. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } /// Counterpart to [`HttpRequest::app_config`]. #[inline] pub fn app_config(&self) -> &AppConfig { self.req.app_config() } /// Counterpart to [`HttpRequest::app_data`]. #[inline] pub fn app_data(&self) -> Option<&T> { for container in self.req.inner.app_data.iter().rev() { if let Some(data) = container.get::() { return Some(data); } } None } /// Counterpart to [`HttpRequest::conn_data`]. #[inline] pub fn conn_data(&self) -> Option<&T> { self.req.conn_data() } /// Return request cookies. #[cfg(feature = "cookies")] #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { self.req.cookies() } /// Return request cookie. #[cfg(feature = "cookies")] #[inline] pub fn cookie(&self, name: &str) -> Option> { self.req.cookie(name) } /// Set request payload. #[inline] pub fn set_payload(&mut self, payload: Payload) { self.payload = payload; } /// Add data container to request's resolution set. /// /// In middleware, prefer [`extensions_mut`](ServiceRequest::extensions_mut) for request-local /// data since it is assumed that the same app data is presented for every request. pub fn add_data_container(&mut self, extensions: Rc) { Rc::get_mut(&mut (self.req).inner) .unwrap() .app_data .push(extensions); } /// Creates a context object for use with a routing [guard](crate::guard). #[inline] pub fn guard_ctx(&self) -> GuardContext<'_> { GuardContext { req: self } } } impl Resource for ServiceRequest { type Path = Url; #[inline] fn resource_path(&mut self) -> &mut Path { self.match_info_mut() } } impl HttpMessage for ServiceRequest { type Stream = BoxedPayloadStream; #[inline] fn headers(&self) -> &HeaderMap { &self.head().headers } #[inline] fn extensions(&self) -> Ref<'_, Extensions> { self.req.extensions() } #[inline] fn extensions_mut(&self) -> RefMut<'_, Extensions> { self.req.extensions_mut() } #[inline] fn take_payload(&mut self) -> Payload { self.payload.take() } } impl fmt::Debug for ServiceRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( f, "\nServiceRequest {:?} {}:{}", self.head().version, self.head().method, self.path() )?; if !self.query_string().is_empty() { writeln!(f, " query: ?{:?}", self.query_string())?; } if !self.match_info().is_empty() { writeln!(f, " params: {:?}", self.match_info())?; } writeln!(f, " headers:")?; for (key, val) in self.headers().iter() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) } } /// A service level response wrapper. pub struct ServiceResponse { request: HttpRequest, response: HttpResponse, } impl ServiceResponse { /// Create service response from the error pub fn from_err>(err: E, request: HttpRequest) -> Self { let response = HttpResponse::from_error(err); ServiceResponse { request, response } } } impl ServiceResponse { /// Create service response instance pub fn new(request: HttpRequest, response: HttpResponse) -> Self { ServiceResponse { request, response } } /// Create service response for error #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { ServiceResponse::from_err(err, self.request) } /// Create service response #[inline] pub fn into_response(self, response: HttpResponse) -> ServiceResponse { ServiceResponse::new(self.request, response) } /// Returns reference to original request. #[inline] pub fn request(&self) -> &HttpRequest { &self.request } /// Returns reference to response. #[inline] pub fn response(&self) -> &HttpResponse { &self.response } /// Returns mutable reference to response. #[inline] pub fn response_mut(&mut self) -> &mut HttpResponse { &mut self.response } /// Returns response status code. #[inline] pub fn status(&self) -> StatusCode { self.response.status() } /// Returns response's headers. #[inline] pub fn headers(&self) -> &HeaderMap { self.response.headers() } /// Returns mutable response's headers. #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } /// Destructures `ServiceResponse` into request and response components. #[inline] pub fn into_parts(self) -> (HttpRequest, HttpResponse) { (self.request, self.response) } /// Map the current body type to another using a closure. Returns a new response. /// /// Closure receives the response head and the current body type. #[inline] pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, B) -> B2, { let response = self.response.map_body(f); ServiceResponse { response, request: self.request, } } #[inline] pub fn map_into_left_body(self) -> ServiceResponse> { self.map_body(|_, body| EitherBody::left(body)) } #[inline] pub fn map_into_right_body(self) -> ServiceResponse> { self.map_body(|_, body| EitherBody::right(body)) } #[inline] pub fn map_into_boxed_body(self) -> ServiceResponse where B: MessageBody + 'static, { self.map_body(|_, body| body.boxed()) } /// Consumes the response and returns its body. #[inline] pub fn into_body(self) -> B { self.response.into_body() } } impl From> for HttpResponse { fn from(res: ServiceResponse) -> HttpResponse { res.response } } impl From> for Response { fn from(res: ServiceResponse) -> Response { res.response.into() } } impl fmt::Debug for ServiceResponse where B: MessageBody, B::Error: Into, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let res = writeln!( f, "\nServiceResponse {:?} {}{}", self.response.head().version, self.response.head().status, self.response.head().reason.unwrap_or(""), ); let _ = writeln!(f, " headers:"); for (key, val) in self.response.head().headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } let _ = writeln!(f, " body: {:?}", self.response.body().size()); res } } pub struct WebService { rdef: Patterns, name: Option, guards: Vec>, } impl WebService { /// Create new `WebService` instance. pub fn new(path: T) -> Self { WebService { rdef: path.patterns(), name: None, guards: Vec::new(), } } /// Set service name. /// /// Name is used for URL generation. pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self } /// Add match guard to a web service. /// /// ``` /// use actix_web::{web, guard, dev, App, Error, HttpResponse}; /// /// async fn index(req: dev::ServiceRequest) -> Result { /// Ok(req.into_response(HttpResponse::Ok().finish())) /// } /// /// let app = App::new() /// .service( /// web::service("/app") /// .guard(guard::Header("content-type", "text/plain")) /// .finish(index) /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); self } /// Set a service factory implementation and generate web service. pub fn finish(self, service: F) -> impl HttpServiceFactory where F: IntoServiceFactory, T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), > + 'static, { WebServiceImpl { srv: service.into_factory(), rdef: self.rdef, name: self.name, guards: self.guards, } } } struct WebServiceImpl { srv: T, rdef: Patterns, name: Option, guards: Vec>, } impl HttpServiceFactory for WebServiceImpl where T: ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = Error, InitError = (), > + 'static, { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { None } else { Some(std::mem::take(&mut self.guards)) }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(ensure_leading_slash(self.rdef)) } else { ResourceDef::new(self.rdef) }; if let Some(ref name) = self.name { rdef.set_name(name); } config.register_service(rdef, guards, self.srv, None) } } /// Macro to help register different types of services at the same time. /// /// The max number of services that can be grouped together is 12 and all must implement the /// [`HttpServiceFactory`] trait. /// /// # Examples /// ``` /// use actix_web::{services, web, App}; /// /// let services = services![ /// web::resource("/test2").to(|| async { "test2" }), /// web::scope("/test3").route("/", web::get().to(|| async { "test3" })) /// ]; /// /// let app = App::new().service(services); /// /// // services macro just convert multiple services to a tuple. /// // below would also work without importing the macro. /// let app = App::new().service(( /// web::resource("/test2").to(|| async { "test2" }), /// web::scope("/test3").route("/", web::get().to(|| async { "test3" })) /// )); /// ``` #[macro_export] macro_rules! services { ($($x:expr),+ $(,)?) => { ($($x,)+) } } /// HttpServiceFactory trait impl for tuples macro_rules! service_tuple ({ $($T:ident)+ } => { impl<$($T: HttpServiceFactory),+> HttpServiceFactory for ($($T,)+) { #[allow(non_snake_case)] fn register(self, config: &mut AppService) { let ($($T,)*) = self; $($T.register(config);)+ } } }); service_tuple! { A } service_tuple! { A B } service_tuple! { A B C } service_tuple! { A B C D } service_tuple! { A B C D E } service_tuple! { A B C D E F } service_tuple! { A B C D E F G } service_tuple! { A B C D E F G H } service_tuple! { A B C D E F G H I } service_tuple! { A B C D E F G H I J } service_tuple! { A B C D E F G H I J K } service_tuple! { A B C D E F G H I J K L } #[cfg(test)] mod tests { use actix_service::Service; use actix_utils::future::ok; use super::*; use crate::{ guard, http, test::{self, init_service, TestRequest}, web, App, }; #[actix_rt::test] async fn test_service() { let srv = init_service( App::new().service(web::service("/test").name("test").finish( |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), )), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let srv = init_service( App::new().service(web::service("/test").guard(guard::Get()).finish( |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), )), ) .await; let req = TestRequest::with_uri("/test") .method(http::Method::PUT) .to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { let srv = init_service( App::new() .data(42u32) .service( web::service("/test") .name("test") .finish(|req: ServiceRequest| { assert_eq!(req.app_data::>().unwrap().as_ref(), &42); ok(req.into_response(HttpResponse::Ok().finish())) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); } #[test] fn test_fmt_debug() { let req = TestRequest::get() .uri("/index.html?test=1") .insert_header(("x-test", "111")) .to_srv_request(); let s = format!("{:?}", req); assert!(s.contains("ServiceRequest")); assert!(s.contains("test=1")); assert!(s.contains("x-test")); let res = HttpResponse::Ok().insert_header(("x-test", "111")).finish(); let res = TestRequest::post() .uri("/index.html?test=1") .to_srv_response(res); let s = format!("{:?}", res); assert!(s.contains("ServiceResponse")); assert!(s.contains("x-test")); } #[actix_rt::test] async fn test_services_macro() { let scoped = services![ web::service("/scoped_test1").name("scoped_test1").finish( |req: ServiceRequest| async { Ok(req.into_response(HttpResponse::Ok().finish())) } ), web::resource("/scoped_test2").to(|| async { "test2" }), ]; let services = services![ web::service("/test1") .name("test") .finish(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::Ok().finish())) }), web::resource("/test2").to(|| async { "test2" }), web::scope("/test3").service(scoped) ]; let srv = init_service(App::new().service(services)).await; let req = TestRequest::with_uri("/test1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test3/scoped_test1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test3/scoped_test2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); } #[actix_rt::test] async fn test_services_vec() { let services = vec![ web::resource("/test1").to(|| async { "test1" }), web::resource("/test2").to(|| async { "test2" }), ]; let scoped = vec![ web::resource("/scoped_test1").to(|| async { "test1" }), web::resource("/scoped_test2").to(|| async { "test2" }), ]; let srv = init_service( App::new() .service(services) .service(web::scope("/test3").service(scoped)), ) .await; let req = TestRequest::with_uri("/test1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test3/scoped_test1").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); let req = TestRequest::with_uri("/test3/scoped_test2").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); } #[actix_rt::test] #[should_panic(expected = "called `Option::unwrap()` on a `None` value")] async fn cloning_request_panics() { async fn index(_name: web::Path<(String,)>) -> &'static str { "" } let app = test::init_service( App::new() .wrap_fn(|req, svc| { let (req, pl) = req.into_parts(); let _req2 = req.clone(); let req = ServiceRequest::from_parts(req, pl); svc.call(req) }) .route("/", web::get().to(|| async { "" })) .service(web::resource("/resource1/{name}/index.html").route(web::get().to(index))), ) .await; let req = test::TestRequest::default().to_request(); let _res = test::call_service(&app, req).await; } } actix-web-4.9.0/src/test/mod.rs000064400000000000000000000050051046102023000144140ustar 00000000000000//! Various helpers for Actix applications to use during testing. //! //! # Creating A Test Service //! - [`init_service`] //! //! # Off-The-Shelf Test Services //! - [`ok_service`] //! - [`status_service`] //! //! # Calling Test Service //! - [`TestRequest`] //! - [`call_service`] //! - [`try_call_service`] //! - [`call_and_read_body`] //! - [`call_and_read_body_json`] //! - [`try_call_and_read_body_json`] //! //! # Reading Response Payloads //! - [`read_body`] //! - [`try_read_body`] //! - [`read_body_json`] //! - [`try_read_body_json`] // TODO: more docs on generally how testing works with these parts pub use actix_http::test::TestBuffer; mod test_request; mod test_services; mod test_utils; #[allow(deprecated)] pub use self::test_services::{default_service, ok_service, simple_service, status_service}; #[cfg(test)] pub(crate) use self::test_utils::try_init_service; #[allow(deprecated)] pub use self::test_utils::{read_response, read_response_json}; pub use self::{ test_request::TestRequest, test_utils::{ call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, read_body_json, try_call_and_read_body_json, try_call_service, try_read_body, try_read_body_json, }, }; /// Reduces boilerplate code when testing expected response payloads. /// /// Must be used inside an async test. Works for both `ServiceRequest` and `HttpRequest`. /// /// # Examples /// ``` /// use actix_web::{http::StatusCode, HttpResponse}; /// /// let res = HttpResponse::with_body(StatusCode::OK, "http response"); /// assert_body_eq!(res, b"http response"); /// ``` #[cfg(test)] macro_rules! assert_body_eq { ($res:ident, $expected:expr) => { assert_eq!( ::actix_http::body::to_bytes($res.into_body()) .await .expect("error reading test response body"), ::bytes::Bytes::from_static($expected), ) }; } #[cfg(test)] pub(crate) use assert_body_eq; #[cfg(test)] mod tests { use super::*; use crate::{http::StatusCode, service::ServiceResponse, HttpResponse}; #[actix_rt::test] async fn assert_body_works_for_service_and_regular_response() { let res = HttpResponse::with_body(StatusCode::OK, "http response"); assert_body_eq!(res, b"http response"); let req = TestRequest::default().to_http_request(); let res = HttpResponse::with_body(StatusCode::OK, "service response"); let res = ServiceResponse::new(req, res); assert_body_eq!(res, b"service response"); } } actix-web-4.9.0/src/test/test_request.rs000064400000000000000000000331711046102023000163710ustar 00000000000000use std::{borrow::Cow, net::SocketAddr, rc::Rc}; use actix_http::{test::TestRequest as HttpTestRequest, Request}; use serde::Serialize; #[cfg(feature = "cookies")] use crate::cookie::{Cookie, CookieJar}; use crate::{ app_service::AppInitServiceState, config::AppConfig, data::Data, dev::{Extensions, Path, Payload, ResourceDef, Service, Url}, http::{ header::{ContentType, TryIntoHeaderPair}, Method, Uri, Version, }, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, test, web::Bytes, HttpRequest, HttpResponse, }; /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. /// You can generate various types of request via TestRequest's methods: /// - [`TestRequest::to_request`] creates an [`actix_http::Request`](Request). /// - [`TestRequest::to_srv_request`] creates a [`ServiceRequest`], which is used for testing middlewares and chain adapters. /// - [`TestRequest::to_srv_response`] creates a [`ServiceResponse`]. /// - [`TestRequest::to_http_request`] creates an [`HttpRequest`], which is used for testing handlers. /// /// ``` /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; /// /// async fn handler(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { /// HttpResponse::Ok().into() /// } else { /// HttpResponse::BadRequest().into() /// } /// } /// /// #[actix_web::test] /// # // force rustdoc to display the correct thing and also compile check the test /// # async fn _test() {} /// async fn test_index() { /// let req = test::TestRequest::default() /// .insert_header(header::ContentType::plaintext()) /// .to_http_request(); /// /// let resp = handler(req).await; /// assert_eq!(resp.status(), StatusCode::OK); /// /// let req = test::TestRequest::default().to_http_request(); /// let resp = handler(req).await; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` pub struct TestRequest { req: HttpTestRequest, rmap: ResourceMap, config: AppConfig, path: Path, peer_addr: Option, app_data: Extensions, #[cfg(feature = "cookies")] cookies: CookieJar, } impl Default for TestRequest { fn default() -> TestRequest { TestRequest { req: HttpTestRequest::default(), rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfig::default(), path: Path::new(Url::new(Uri::default())), peer_addr: None, app_data: Extensions::new(), #[cfg(feature = "cookies")] cookies: CookieJar::new(), } } } #[allow(clippy::wrong_self_convention)] impl TestRequest { /// Constructs test request and sets request URI. pub fn with_uri(uri: &str) -> TestRequest { TestRequest::default().uri(uri) } /// Constructs test request with GET method. pub fn get() -> TestRequest { TestRequest::default().method(Method::GET) } /// Constructs test request with POST method. pub fn post() -> TestRequest { TestRequest::default().method(Method::POST) } /// Constructs test request with PUT method. pub fn put() -> TestRequest { TestRequest::default().method(Method::PUT) } /// Constructs test request with PATCH method. pub fn patch() -> TestRequest { TestRequest::default().method(Method::PATCH) } /// Constructs test request with DELETE method. pub fn delete() -> TestRequest { TestRequest::default().method(Method::DELETE) } /// Sets HTTP version of this request. pub fn version(mut self, ver: Version) -> Self { self.req.version(ver); self } /// Sets method of this request. pub fn method(mut self, meth: Method) -> Self { self.req.method(meth); self } /// Sets URI of this request. pub fn uri(mut self, path: &str) -> Self { self.req.uri(path); self } /// Inserts a header, replacing any that were set with an equivalent field name. pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { self.req.insert_header(header); self } /// Appends a header, keeping any that were set with an equivalent field name. pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { self.req.append_header(header); self } /// Sets cookie for this request. #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { self.cookies.add(cookie.into_owned()); self } /// Sets request path pattern parameter. /// /// # Examples /// /// ``` /// use actix_web::test::TestRequest; /// /// let req = TestRequest::default().param("foo", "bar"); /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); /// ``` pub fn param( mut self, name: impl Into>, value: impl Into>, ) -> Self { self.path.add_static(name, value); self } /// Sets peer address. pub fn peer_addr(mut self, addr: SocketAddr) -> Self { self.peer_addr = Some(addr); self } /// Sets request payload. pub fn set_payload(mut self, data: impl Into) -> Self { self.req.set_payload(data); self } /// Serializes `data` to a URL encoded form and set it as the request payload. /// /// The `Content-Type` header is set to `application/x-www-form-urlencoded`. pub fn set_form(mut self, data: impl Serialize) -> Self { let bytes = serde_urlencoded::to_string(&data) .expect("Failed to serialize test data as a urlencoded form"); self.req.set_payload(bytes); self.req.insert_header(ContentType::form_url_encoded()); self } /// Serializes `data` to JSON and set it as the request payload. /// /// The `Content-Type` header is set to `application/json`. pub fn set_json(mut self, data: impl Serialize) -> Self { let bytes = serde_json::to_string(&data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self } /// Inserts application data. /// /// This is equivalent of `App::app_data()` method for testing purpose. pub fn app_data(mut self, data: T) -> Self { self.app_data.insert(data); self } /// Inserts application data. /// /// This is equivalent of `App::data()` method for testing purpose. #[doc(hidden)] pub fn data(mut self, data: T) -> Self { self.app_data.insert(Data::new(data)); self } /// Sets resource map. #[cfg(test)] pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { self.rmap = rmap; self } /// Finalizes test request. /// /// This request builder will be useless after calling `finish()`. fn finish(&mut self) -> Request { // mut used when cookie feature is enabled #[allow(unused_mut)] let mut req = self.req.finish(); #[cfg(feature = "cookies")] { use actix_http::header::{HeaderValue, COOKIE}; let cookie: String = self .cookies .delta() // ensure only name=value is written to cookie header .map(|c| c.stripped().encoded().to_string()) .collect::>() .join("; "); if !cookie.is_empty() { req.headers_mut() .insert(COOKIE, HeaderValue::from_str(&cookie).unwrap()); } } req } /// Finalizes request creation and returns `Request` instance. pub fn to_request(mut self) -> Request { let mut req = self.finish(); req.head_mut().peer_addr = self.peer_addr; req } /// Finalizes request creation and returns `ServiceRequest` instance. pub fn to_srv_request(mut self) -> ServiceRequest { let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); ServiceRequest::new( HttpRequest::new( self.path, head, app_state, Rc::new(self.app_data), None, Default::default(), ), payload, ) } /// Finalizes request creation and returns `ServiceResponse` instance. pub fn to_srv_response(self, res: HttpResponse) -> ServiceResponse { self.to_srv_request().into_response(res) } /// Finalizes request creation and returns `HttpRequest` instance. pub fn to_http_request(mut self) -> HttpRequest { let (mut head, _) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); HttpRequest::new( self.path, head, app_state, Rc::new(self.app_data), None, Default::default(), ) } /// Finalizes request creation and returns `HttpRequest` and `Payload` pair. pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); let req = HttpRequest::new( self.path, head, app_state, Rc::new(self.app_data), None, Default::default(), ); (req, payload) } /// Finalizes request creation, calls service, and waits for response future completion. pub async fn send_request(self, app: &S) -> S::Response where S: Service, Error = E>, E: std::fmt::Debug, { let req = self.to_request(); test::call_service(app, req).await } #[cfg(test)] pub fn set_server_hostname(&mut self, host: &str) { self.config.set_host(host) } } #[cfg(test)] mod tests { use std::time::SystemTime; use super::*; use crate::{http::header, test::init_service, web, App, Error, Responder}; #[actix_rt::test] async fn test_basics() { let req = TestRequest::default() .version(Version::HTTP_2) .insert_header(header::ContentType::json()) .insert_header(header::Date(SystemTime::now().into())) .param("test", "123") .data(10u32) .app_data(20u64) .peer_addr("127.0.0.1:8081".parse().unwrap()) .to_http_request(); assert!(req.headers().contains_key(header::CONTENT_TYPE)); assert!(req.headers().contains_key(header::DATE)); assert_eq!( req.head().peer_addr, Some("127.0.0.1:8081".parse().unwrap()) ); assert_eq!(&req.match_info()["test"], "123"); assert_eq!(req.version(), Version::HTTP_2); let data = req.app_data::>().unwrap(); assert!(req.app_data::>().is_none()); assert_eq!(*data.get_ref(), 10); assert!(req.app_data::().is_none()); let data = req.app_data::().unwrap(); assert_eq!(*data, 20); } #[actix_rt::test] async fn test_send_request() { let app = init_service( App::new().service( web::resource("/index.html") .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), ), ) .await; let resp = TestRequest::get() .uri("/index.html") .send_request(&app) .await; let result = test::read_body(resp).await; assert_eq!(result, Bytes::from_static(b"welcome!")); } #[actix_rt::test] async fn test_async_with_block() { async fn async_with_block() -> Result { let res = web::block(move || Some(4usize).ok_or("wrong")).await; match res { Ok(value) => Ok(HttpResponse::Ok() .content_type("text/plain") .body(format!("Async with block value: {:?}", value))), Err(_) => panic!("Unexpected"), } } let app = init_service(App::new().service(web::resource("/index.html").to(async_with_block))) .await; let req = TestRequest::post().uri("/index.html").to_request(); let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_server_data() { async fn handler(data: web::Data) -> impl Responder { assert_eq!(**data, 10); HttpResponse::Ok() } let app = init_service( App::new() .data(10usize) .service(web::resource("/index.html").to(handler)), ) .await; let req = TestRequest::post().uri("/index.html").to_request(); let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } } actix-web-4.9.0/src/test/test_services.rs000064400000000000000000000023631046102023000165230ustar 00000000000000use actix_utils::future::ok; use crate::{ body::BoxBody, dev::{fn_service, Service, ServiceRequest, ServiceResponse}, http::StatusCode, Error, HttpResponseBuilder, }; /// Creates service that always responds with `200 OK` and no body. pub fn ok_service( ) -> impl Service, Error = Error> { status_service(StatusCode::OK) } /// Creates service that always responds with given status code and no body. pub fn status_service( status_code: StatusCode, ) -> impl Service, Error = Error> { fn_service(move |req: ServiceRequest| { ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `status_service`.")] pub fn simple_service( status_code: StatusCode, ) -> impl Service, Error = Error> { status_service(status_code) } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `status_service`.")] pub fn default_service( status_code: StatusCode, ) -> impl Service, Error = Error> { status_service(status_code) } actix-web-4.9.0/src/test/test_utils.rs000064400000000000000000000440701046102023000160410ustar 00000000000000use std::error::Error as StdError; use actix_http::Request; use actix_service::IntoServiceFactory; use serde::de::DeserializeOwned; use crate::{ body::{self, MessageBody}, config::AppConfig, dev::{Service, ServiceFactory}, service::ServiceResponse, web::Bytes, Error, }; /// Initialize service from application builder instance. /// /// # Examples /// ``` /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// /// #[actix_web::test] /// async fn test_init_service() { /// let app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| async { "OK" })) /// ).await; /// /// // Create request object /// let req = test::TestRequest::with_uri("/test").to_request(); /// /// // Execute application /// let res = app.call(req).await.unwrap(); /// assert_eq!(res.status(), StatusCode::OK); /// } /// ``` /// /// # Panics /// Panics if service initialization returns an error. pub async fn init_service( app: R, ) -> impl Service, Error = E> where R: IntoServiceFactory, S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { try_init_service(app) .await .expect("service initialization failed") } /// Fallible version of [`init_service`] that allows testing initialization errors. pub(crate) async fn try_init_service( app: R, ) -> Result, Error = E>, S::InitError> where R: IntoServiceFactory, S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { let srv = app.into_factory(); srv.new_service(AppConfig::default()).await } /// Calls service and waits for response future completion. /// /// # Examples /// ``` /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// /// #[actix_web::test] /// async fn test_response() { /// let app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| async { /// HttpResponse::Ok() /// })) /// ).await; /// /// // Create request object /// let req = test::TestRequest::with_uri("/test").to_request(); /// /// // Call application /// let res = test::call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::OK); /// } /// ``` /// /// # Panics /// Panics if service call returns error. To handle errors use `app.call(req)`. pub async fn call_service(app: &S, req: R) -> S::Response where S: Service, Error = E>, E: std::fmt::Debug, { app.call(req) .await .expect("test service call returned error") } /// Fallible version of [`call_service`] that allows testing response completion errors. pub async fn try_call_service(app: &S, req: R) -> Result where S: Service, Error = E>, E: std::fmt::Debug, { app.call(req).await } /// Helper function that returns a response body of a TestRequest /// /// # Examples /// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// /// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( /// web::resource("/index.html") /// .route(web::post().to(|| async { /// HttpResponse::Ok().body("welcome!") /// }))) /// ).await; /// /// let req = test::TestRequest::post() /// .uri("/index.html") /// .header(header::CONTENT_TYPE, "application/json") /// .to_request(); /// /// let result = test::call_and_read_body(&app, req).await; /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } /// ``` /// /// # Panics /// Panics if: /// - service call returns error; /// - body yields an error while it is being read. pub async fn call_and_read_body(app: &S, req: Request) -> Bytes where S: Service, Error = Error>, B: MessageBody, { let res = call_service(app, req).await; read_body(res).await } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body`.")] pub async fn read_response(app: &S, req: Request) -> Bytes where S: Service, Error = Error>, B: MessageBody, { let res = call_service(app, req).await; read_body(res).await } /// Helper function that returns a response body of a ServiceResponse. /// /// # Examples /// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// /// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( /// web::resource("/index.html") /// .route(web::post().to(|| async { /// HttpResponse::Ok().body("welcome!") /// }))) /// ).await; /// /// let req = test::TestRequest::post() /// .uri("/index.html") /// .header(header::CONTENT_TYPE, "application/json") /// .to_request(); /// /// let res = test::call_service(&app, req).await; /// let result = test::read_body(res).await; /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } /// ``` /// /// # Panics /// Panics if body yields an error while it is being read. pub async fn read_body(res: ServiceResponse) -> Bytes where B: MessageBody, { try_read_body(res) .await .map_err(Into::>::into) .expect("error reading test response body") } /// Fallible version of [`read_body`] that allows testing MessageBody reading errors. pub async fn try_read_body(res: ServiceResponse) -> Result::Error> where B: MessageBody, { let body = res.into_body(); body::to_bytes(body).await } /// Helper function that returns a deserialized response body of a ServiceResponse. /// /// # Examples /// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize)] /// pub struct Person { /// id: String, /// name: String, /// } /// /// #[actix_web::test] /// async fn test_post_person() { /// let app = test::init_service( /// App::new().service( /// web::resource("/people") /// .route(web::post().to(|person: web::Json| async { /// HttpResponse::Ok() /// .json(person)}) /// )) /// ).await; /// /// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); /// /// let res = test::TestRequest::post() /// .uri("/people") /// .header(header::CONTENT_TYPE, "application/json") /// .set_payload(payload) /// .send_request(&mut app) /// .await; /// /// assert!(res.status().is_success()); /// /// let result: Person = test::read_body_json(res).await; /// } /// ``` /// /// # Panics /// Panics if: /// - body yields an error while it is being read; /// - received body is not a valid JSON representation of `T`. pub async fn read_body_json(res: ServiceResponse) -> T where B: MessageBody, T: DeserializeOwned, { try_read_body_json(res).await.unwrap_or_else(|err| { panic!( "could not deserialize body into a {}\nerr: {}", std::any::type_name::(), err, ) }) } /// Fallible version of [`read_body_json`] that allows testing response deserialization errors. pub async fn try_read_body_json(res: ServiceResponse) -> Result> where B: MessageBody, T: DeserializeOwned, { let body = try_read_body(res) .await .map_err(Into::>::into)?; serde_json::from_slice(&body).map_err(Into::>::into) } /// Helper function that returns a deserialized response body of a TestRequest /// /// # Examples /// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// /// #[derive(Serialize, Deserialize)] /// pub struct Person { /// id: String, /// name: String /// } /// /// #[actix_web::test] /// async fn test_add_person() { /// let app = test::init_service( /// App::new().service( /// web::resource("/people") /// .route(web::post().to(|person: web::Json| async { /// HttpResponse::Ok() /// .json(person)}) /// )) /// ).await; /// /// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); /// /// let req = test::TestRequest::post() /// .uri("/people") /// .header(header::CONTENT_TYPE, "application/json") /// .set_payload(payload) /// .to_request(); /// /// let result: Person = test::call_and_read_body_json(&mut app, req).await; /// } /// ``` /// /// # Panics /// Panics if: /// - service call returns an error body yields an error while it is being read; /// - body yields an error while it is being read; /// - received body is not a valid JSON representation of `T`. pub async fn call_and_read_body_json(app: &S, req: Request) -> T where S: Service, Error = Error>, B: MessageBody, T: DeserializeOwned, { try_call_and_read_body_json(app, req).await.unwrap() } /// Fallible version of [`call_and_read_body_json`] that allows testing service call errors. pub async fn try_call_and_read_body_json( app: &S, req: Request, ) -> Result> where S: Service, Error = Error>, B: MessageBody, T: DeserializeOwned, { let res = try_call_service(app, req) .await .map_err(Into::>::into)?; try_read_body_json(res).await } #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body_json`.")] pub async fn read_response_json(app: &S, req: Request) -> T where S: Service, Error = Error>, B: MessageBody, T: DeserializeOwned, { call_and_read_body_json(app, req).await } #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; use super::*; use crate::{ dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage, HttpResponse, }; #[actix_rt::test] async fn test_request_methods() { let app = init_service( App::new().service( web::resource("/index.html") .route(web::put().to(|| HttpResponse::Ok().body("put!"))) .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))) .route(web::delete().to(|| HttpResponse::Ok().body("delete!"))), ), ) .await; let put_req = TestRequest::put() .uri("/index.html") .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); let result = call_and_read_body(&app, put_req).await; assert_eq!(result, Bytes::from_static(b"put!")); let patch_req = TestRequest::patch() .uri("/index.html") .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); let result = call_and_read_body(&app, patch_req).await; assert_eq!(result, Bytes::from_static(b"patch!")); let delete_req = TestRequest::delete().uri("/index.html").to_request(); let result = call_and_read_body(&app, delete_req).await; assert_eq!(result, Bytes::from_static(b"delete!")); } #[derive(Serialize, Deserialize, Debug)] pub struct Person { id: String, name: String, } #[actix_rt::test] async fn test_response_json() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); let req = TestRequest::post() .uri("/people") .insert_header((header::CONTENT_TYPE, "application/json")) .set_payload(payload) .to_request(); let result: Person = call_and_read_body_json(&app, req).await; assert_eq!(&result.id, "12345"); } #[actix_rt::test] async fn test_try_response_json_error() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); let req = TestRequest::post() .uri("/animals") // Not registered to ensure an error occurs. .insert_header((header::CONTENT_TYPE, "application/json")) .set_payload(payload) .to_request(); let result: Result> = try_call_and_read_body_json(&app, req).await; assert!(result.is_err()); } #[actix_rt::test] async fn test_body_json() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); let res = TestRequest::post() .uri("/people") .insert_header((header::CONTENT_TYPE, "application/json")) .set_payload(payload) .send_request(&app) .await; let result: Person = read_body_json(res).await; assert_eq!(&result.name, "User name"); } #[actix_rt::test] async fn test_try_body_json_error() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; // Use a number for id to cause a deserialization error. let payload = r#"{"id":12345,"name":"User name"}"#.as_bytes(); let res = TestRequest::post() .uri("/people") .insert_header((header::CONTENT_TYPE, "application/json")) .set_payload(payload) .send_request(&app) .await; let result: Result> = try_read_body_json(res).await; assert!(result.is_err()); } #[actix_rt::test] async fn test_request_response_form() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), ))) .await; let payload = Person { id: "12345".to_string(), name: "User name".to_string(), }; let req = TestRequest::post() .uri("/people") .set_form(&payload) .to_request(); assert_eq!(req.content_type(), "application/x-www-form-urlencoded"); let result: Person = call_and_read_body_json(&app, req).await; assert_eq!(&result.id, "12345"); assert_eq!(&result.name, "User name"); } #[actix_rt::test] async fn test_response() { let app = init_service( App::new().service( web::resource("/index.html") .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), ), ) .await; let req = TestRequest::post() .uri("/index.html") .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); let result = call_and_read_body(&app, req).await; assert_eq!(result, Bytes::from_static(b"welcome!")); } #[actix_rt::test] async fn test_request_response_json() { let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; let payload = Person { id: "12345".to_string(), name: "User name".to_string(), }; let req = TestRequest::post() .uri("/people") .set_json(&payload) .to_request(); assert_eq!(req.content_type(), "application/json"); let result: Person = call_and_read_body_json(&app, req).await; assert_eq!(&result.id, "12345"); assert_eq!(&result.name, "User name"); } #[actix_rt::test] #[allow(dead_code)] async fn return_opaque_types() { fn test_app() -> App< impl ServiceFactory< ServiceRequest, Config = (), Response = ServiceResponse, Error = crate::Error, InitError = (), >, > { App::new().service( web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ), ) } async fn test_service( ) -> impl Service, Error = crate::Error> { init_service(test_app()).await } async fn compile_test(mut req: Vec) { let svc = test_service().await; call_service(&svc, req.pop().unwrap()).await; call_and_read_body(&svc, req.pop().unwrap()).await; read_body(call_service(&svc, req.pop().unwrap()).await).await; let _: String = call_and_read_body_json(&svc, req.pop().unwrap()).await; let _: String = read_body_json(call_service(&svc, req.pop().unwrap()).await).await; } } } actix-web-4.9.0/src/thin_data.rs000064400000000000000000000074041046102023000146160ustar 00000000000000use std::any::type_name; use actix_utils::future::{ready, Ready}; use crate::{dev::Payload, error, FromRequest, HttpRequest}; /// Application data wrapper and extractor for cheaply-cloned types. /// /// Similar to the [`Data`] wrapper but for `Clone`/`Copy` types that are already an `Arc` internally, /// share state using some other means when cloned, or is otherwise static data that is very cheap /// to clone. /// /// Unlike `Data`, this wrapper clones `T` during extraction. Therefore, it is the user's /// responsibility to ensure that clones of `T` do actually share the same state, otherwise state /// may be unexpectedly different across multiple requests. /// /// Note that if your type is literally an `Arc` then it's recommended to use the /// [`Data::from(arc)`][data_from_arc] conversion instead. /// /// # Examples /// /// ``` /// use actix_web::{ /// web::{self, ThinData}, /// App, HttpResponse, Responder, /// }; /// /// // Use the `ThinData` extractor to access a database connection pool. /// async fn index(ThinData(db_pool): ThinData) -> impl Responder { /// // database action ... /// /// HttpResponse::Ok() /// } /// /// # type DbPool = (); /// let db_pool = DbPool::default(); /// /// App::new() /// .app_data(ThinData(db_pool.clone())) /// .service(web::resource("/").get(index)) /// # ; /// ``` /// /// [`Data`]: crate::web::Data /// [data_from_arc]: crate::web::Data#impl-From>-for-Data #[derive(Debug, Clone)] pub struct ThinData(pub T); impl_more::impl_as_ref!(ThinData => T); impl_more::impl_as_mut!(ThinData => T); impl_more::impl_deref_and_mut!( in ThinData => T); impl FromRequest for ThinData { type Error = crate::Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ready(req.app_data::().cloned().ok_or_else(|| { log::debug!( "Failed to extract `ThinData<{}>` for `{}` handler. For the ThinData extractor to work \ correctly, wrap the data with `ThinData()` and pass it to `App::app_data()`. \ Ensure that types align in both the set and retrieve calls.", type_name::(), req.match_name().unwrap_or(req.path()) ); error::ErrorInternalServerError( "Requested application data is not configured correctly. \ View/enable debug logs for more details.", ) })) } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; use super::*; use crate::{ http::StatusCode, test::{call_service, init_service, TestRequest}, web, App, HttpResponse, }; type TestT = Arc>; #[actix_rt::test] async fn thin_data() { let test_data = TestT::default(); let app = init_service(App::new().app_data(ThinData(test_data.clone())).service( web::resource("/").to(|td: ThinData| { *td.lock().unwrap() += 1; HttpResponse::Ok() }), )) .await; for _ in 0..3 { let req = TestRequest::default().to_request(); let resp = call_service(&app, req).await; assert_eq!(resp.status(), StatusCode::OK); } assert_eq!(*test_data.lock().unwrap(), 3); } #[actix_rt::test] async fn thin_data_missing() { let app = init_service( App::new().service(web::resource("/").to(|_: ThinData| HttpResponse::Ok())), ) .await; let req = TestRequest::default().to_request(); let resp = call_service(&app, req).await; assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } } actix-web-4.9.0/src/types/either.rs000064400000000000000000000245431046102023000153120ustar 00000000000000//! For either helper, see [`Either`]. use std::{ future::Future, mem, pin::Pin, task::{Context, Poll}, }; use bytes::Bytes; use futures_core::ready; use pin_project_lite::pin_project; use crate::{ body::EitherBody, dev, web::{Form, Json}, Error, FromRequest, HttpRequest, HttpResponse, Responder, }; /// Combines two extractor or responder types into a single type. /// /// # Extractor /// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for /// "polymorphic payloads" where, for example, a form might be JSON or URL encoded. /// /// It is important to note that this extractor, by necessity, buffers the entire request payload /// as part of its implementation. Though, it does respect any `PayloadConfig` maximum size limits. /// /// ``` /// use actix_web::{post, web, Either}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// name: String, /// } /// /// // handler that accepts form as JSON or form-urlencoded. /// #[post("/")] /// async fn index(form: Either, web::Form>) -> String { /// let name: String = match form { /// Either::Left(json) => json.name.to_owned(), /// Either::Right(form) => form.name.to_owned(), /// }; /// /// format!("Welcome {}!", name) /// } /// ``` /// /// # Responder /// It may be desirable to use a concrete type for a response with multiple branches. As long as /// both types implement `Responder`, so will the `Either` type, enabling it to be used as a /// handler's return type. /// /// All properties of a response are determined by the Responder branch returned. /// /// ``` /// use actix_web::{get, Either, Error, HttpResponse}; /// /// #[get("/")] /// async fn index() -> Either<&'static str, Result> { /// if 1 == 2 { /// // respond with Left variant /// Either::Left("Bad data") /// } else { /// // respond with Right variant /// Either::Right( /// Ok(HttpResponse::Ok() /// .content_type(mime::TEXT_HTML) /// .body("

Hello!

")) /// ) /// } /// } /// ``` #[derive(Debug, PartialEq, Eq)] pub enum Either { /// A value of type `L`. Left(L), /// A value of type `R`. Right(R), } impl Either, Json> { pub fn into_inner(self) -> T { match self { Either::Left(form) => form.into_inner(), Either::Right(form) => form.into_inner(), } } } impl Either, Form> { pub fn into_inner(self) -> T { match self { Either::Left(form) => form.into_inner(), Either::Right(form) => form.into_inner(), } } } #[cfg(test)] impl Either { pub(self) fn unwrap_left(self) -> L { match self { Either::Left(data) => data, Either::Right(_) => { panic!("Cannot unwrap Left branch. Either contains an `R` type.") } } } pub(self) fn unwrap_right(self) -> R { match self { Either::Left(_) => { panic!("Cannot unwrap Right branch. Either contains an `L` type.") } Either::Right(data) => data, } } } /// See [here](#responder) for example of usage as a handler return type. impl Responder for Either where L: Responder, R: Responder, { type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { Either::Left(a) => a.respond_to(req).map_into_left_body(), Either::Right(b) => b.respond_to(req).map_into_right_body(), } } } /// A composite error resulting from failure to extract an `Either`. /// /// The implementation of `Into` will return the payload buffering error or the /// error from the primary extractor. To access the fallback error, use a match clause. #[derive(Debug)] pub enum EitherExtractError { /// Error from payload buffering, such as exceeding payload max size limit. Bytes(Error), /// Error from primary and fallback extractors. Extract(L, R), } impl From> for Error where L: Into, R: Into, { fn from(err: EitherExtractError) -> Error { match err { EitherExtractError::Bytes(err) => err, EitherExtractError::Extract(a_err, _b_err) => a_err.into(), } } } /// See [here](#extractor) for example of usage as an extractor. impl FromRequest for Either where L: FromRequest + 'static, R: FromRequest + 'static, { type Error = EitherExtractError; type Future = EitherExtractFut; fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { EitherExtractFut { req: req.clone(), state: EitherExtractState::Bytes { bytes: Bytes::from_request(req, payload), }, } } } pin_project! { pub struct EitherExtractFut where R: FromRequest, L: FromRequest, { req: HttpRequest, #[pin] state: EitherExtractState, } } pin_project! { #[project = EitherExtractProj] pub enum EitherExtractState where L: FromRequest, R: FromRequest, { Bytes { #[pin] bytes: ::Future, }, Left { #[pin] left: L::Future, fallback: Bytes, }, Right { #[pin] right: R::Future, left_err: Option, }, } } impl Future for EitherExtractFut where L: FromRequest, R: FromRequest, LF: Future> + 'static, RF: Future> + 'static, LE: Into, RE: Into, { type Output = Result, EitherExtractError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.project(); let ready = loop { let next = match this.state.as_mut().project() { EitherExtractProj::Bytes { bytes } => { let res = ready!(bytes.poll(cx)); match res { Ok(bytes) => { let fallback = bytes.clone(); let left = L::from_request(this.req, &mut payload_from_bytes(bytes)); EitherExtractState::Left { left, fallback } } Err(err) => break Err(EitherExtractError::Bytes(err)), } } EitherExtractProj::Left { left, fallback } => { let res = ready!(left.poll(cx)); match res { Ok(extracted) => break Ok(Either::Left(extracted)), Err(left_err) => { let right = R::from_request( this.req, &mut payload_from_bytes(mem::take(fallback)), ); EitherExtractState::Right { left_err: Some(left_err), right, } } } } EitherExtractProj::Right { right, left_err } => { let res = ready!(right.poll(cx)); match res { Ok(data) => break Ok(Either::Right(data)), Err(err) => { break Err(EitherExtractError::Extract(left_err.take().unwrap(), err)); } } } }; this.state.set(next); }; Poll::Ready(ready) } } fn payload_from_bytes(bytes: Bytes) -> dev::Payload { let (_, mut h1_payload) = actix_http::h1::Payload::create(true); h1_payload.unread_data(bytes); dev::Payload::from(h1_payload) } #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; use super::*; use crate::test::TestRequest; #[derive(Debug, Clone, Serialize, Deserialize)] struct TestForm { hello: String, } #[actix_rt::test] async fn test_either_extract_first_try() { let (req, mut pl) = TestRequest::default() .set_form(TestForm { hello: "world".to_owned(), }) .to_http_parts(); let form = Either::, Json>::from_request(&req, &mut pl) .await .unwrap() .unwrap_left() .into_inner(); assert_eq!(&form.hello, "world"); } #[actix_rt::test] async fn test_either_extract_fallback() { let (req, mut pl) = TestRequest::default() .set_json(TestForm { hello: "world".to_owned(), }) .to_http_parts(); let form = Either::, Json>::from_request(&req, &mut pl) .await .unwrap() .unwrap_right() .into_inner(); assert_eq!(&form.hello, "world"); } #[actix_rt::test] async fn test_either_extract_recursive_fallback() { let (req, mut pl) = TestRequest::default() .set_payload(Bytes::from_static(b"!@$%^&*()")) .to_http_parts(); let payload = Either::, Json>, Bytes>::from_request(&req, &mut pl) .await .unwrap() .unwrap_right(); assert_eq!(&payload.as_ref(), &b"!@$%^&*()"); } #[actix_rt::test] async fn test_either_extract_recursive_fallback_inner() { let (req, mut pl) = TestRequest::default() .set_json(TestForm { hello: "world".to_owned(), }) .to_http_parts(); let form = Either::, Json>, Bytes>::from_request(&req, &mut pl) .await .unwrap() .unwrap_left() .unwrap_right() .into_inner(); assert_eq!(&form.hello, "world"); } } actix-web-4.9.0/src/types/form.rs000064400000000000000000000401571046102023000147740ustar 00000000000000//! For URL encoded form helper documentation, see [`Form`]. use std::{ borrow::Cow, fmt, future::Future, ops, pin::Pin, rc::Rc, task::{Context, Poll}, }; use actix_http::Payload; use bytes::BytesMut; use encoding_rs::{Encoding, UTF_8}; use futures_core::{future::LocalBoxFuture, ready}; use futures_util::{FutureExt as _, StreamExt as _}; use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ body::EitherBody, error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; /// URL encoded payload extractor and responder. /// /// `Form` has two uses: URL encoded responses, and extracting typed data from URL request payloads. /// /// # Extractor /// To extract typed data from a request body, the inner type `T` must implement the /// [`DeserializeOwned`] trait. /// /// Use [`FormConfig`] to configure extraction options. /// /// ## Examples /// ``` /// use actix_web::{post, web}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// name: String, /// } /// /// // This handler is only called if: /// // - request headers declare the content type as `application/x-www-form-urlencoded` /// // - request payload deserializes into an `Info` struct from the URL encoded format /// #[post("/")] /// async fn index(web::Form(form): web::Form) -> String { /// format!("Welcome {}!", form.name) /// } /// ``` /// /// # Responder /// The `Form` type also allows you to create URL encoded responses by returning a value of type /// `Form` where `T` is the type to be URL encoded, as long as `T` implements [`Serialize`]. /// /// ## Examples /// ``` /// use actix_web::{get, web}; /// use serde::Serialize; /// /// #[derive(Serialize)] /// struct SomeForm { /// name: String, /// age: u8 /// } /// /// // Response will have: /// // - status: 200 OK /// // - header: `Content-Type: application/x-www-form-urlencoded` /// // - body: `name=actix&age=123` /// #[get("/")] /// async fn index() -> web::Form { /// web::Form(SomeForm { /// name: "actix".to_owned(), /// age: 123 /// }) /// } /// ``` /// /// # Panics /// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into /// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Form(pub T); impl Form { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } } impl ops::Deref for Form { type Target = T; fn deref(&self) -> &T { &self.0 } } impl ops::DerefMut for Form { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } impl Serialize for Form where T: Serialize, { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.serialize(serializer) } } /// See [here](#extractor) for example of usage as an extractor. impl FromRequest for Form where T: DeserializeOwned + 'static, { type Error = Error; type Future = FormExtractFut; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone(); FormExtractFut { fut: UrlEncoded::new(req, payload).limit(limit), req: req.clone(), err_handler, } } } type FormErrHandler = Option Error>>; pub struct FormExtractFut { fut: UrlEncoded, err_handler: FormErrHandler, req: HttpRequest, } impl Future for FormExtractFut where T: DeserializeOwned + 'static, { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); let res = ready!(Pin::new(&mut this.fut).poll(cx)); let res = match res { Err(err) => match &this.err_handler { Some(err_handler) => Err((err_handler)(err, &this.req)), None => Err(err.into()), }, Ok(item) => Ok(Form(item)), }; Poll::Ready(res) } } impl fmt::Display for Form { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// See [here](#responder) for example of usage as a handler return type. impl Responder for Form { type Body = EitherBody; fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_urlencoded::to_string(&self.0) { Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_WWW_FORM_URLENCODED) .message_body(body) { Ok(res) => res.map_into_left_body(), Err(err) => HttpResponse::from_error(err).map_into_right_body(), }, Err(err) => { HttpResponse::from_error(UrlencodedError::Serialize(err)).map_into_right_body() } } } } /// [`Form`] extractor configuration. /// /// ``` /// use actix_web::{post, web, App, FromRequest, Result}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// // Custom `FormConfig` is applied to App. /// // Max payload size for URL encoded forms is set to 4kB. /// #[post("/")] /// async fn index(form: web::Form) -> Result { /// Ok(format!("Welcome {}!", form.username)) /// } /// /// App::new() /// .app_data(web::FormConfig::default().limit(4096)) /// .service(index); /// ``` #[derive(Clone)] pub struct FormConfig { limit: usize, err_handler: FormErrHandler, } impl FormConfig { /// Set maximum accepted payload size. By default this limit is 16kB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler pub fn error_handler(mut self, f: F) -> Self where F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { self.err_handler = Some(Rc::new(f)); self } /// Extract payload config from app data. /// /// Checks both `T` and `Data`, in that order, and falls back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } } /// Allow shared refs used as default. const DEFAULT_CONFIG: FormConfig = FormConfig { limit: 16_384, // 2^14 bytes (~16kB) err_handler: None, }; impl Default for FormConfig { fn default() -> Self { DEFAULT_CONFIG } } /// Future that resolves to some `T` when parsed from a URL encoded payload. /// /// Form can be deserialized from any type `T` that implements [`serde::Deserialize`]. /// /// Returns error if: /// - content type is not `application/x-www-form-urlencoded` /// - content length is greater than [limit](UrlEncoded::limit()) pub struct UrlEncoded { #[cfg(feature = "__compress")] stream: Option>, #[cfg(not(feature = "__compress"))] stream: Option, limit: usize, length: Option, encoding: &'static Encoding, err: Option, fut: Option>>, } #[allow(clippy::borrow_interior_mutable_const)] impl UrlEncoded { /// Create a new future to decode a URL encoded request payload. pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); } let encoding = match req.encoding() { Ok(enc) => enc, Err(_) => return Self::err(UrlencodedError::ContentType), }; let mut len = None; if let Some(l) = req.headers().get(&CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) } else { return Self::err(UrlencodedError::UnknownLength); } } else { return Self::err(UrlencodedError::UnknownLength); } }; let payload = { cfg_if::cfg_if! { if #[cfg(feature = "__compress")] { Decompress::from_headers(payload.take(), req.headers()) } else { payload.take() } } }; UrlEncoded { encoding, stream: Some(payload), limit: 32_768, length: len, fut: None, err: None, } } fn err(err: UrlencodedError) -> Self { UrlEncoded { stream: None, limit: 32_768, fut: None, err: Some(err), length: None, encoding: UTF_8, } } /// Set maximum accepted payload size. The default limit is 256kB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } } impl Future for UrlEncoded where T: DeserializeOwned + 'static, { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if let Some(ref mut fut) = self.fut { return Pin::new(fut).poll(cx); } if let Some(err) = self.err.take() { return Poll::Ready(Err(err)); } // payload size let limit = self.limit; if let Some(len) = self.length.take() { if len > limit { return Poll::Ready(Err(UrlencodedError::Overflow { size: len, limit })); } } // future let encoding = self.encoding; let mut stream = self.stream.take().unwrap(); self.fut = Some( async move { let mut body = BytesMut::with_capacity(8192); while let Some(item) = stream.next().await { let chunk = item?; if (body.len() + chunk.len()) > limit { return Err(UrlencodedError::Overflow { size: body.len() + chunk.len(), limit, }); } else { body.extend_from_slice(&chunk); } } if encoding == UTF_8 { serde_urlencoded::from_bytes::(&body).map_err(UrlencodedError::Parse) } else { let body = encoding .decode_without_bom_handling_and_without_replacement(&body) .map(Cow::into_owned) .ok_or(UrlencodedError::Encoding)?; serde_urlencoded::from_str::(&body).map_err(UrlencodedError::Parse) } } .boxed_local(), ); self.poll(cx) } } #[cfg(test)] mod tests { use bytes::Bytes; use serde::{Deserialize, Serialize}; use super::*; use crate::{ http::{ header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, test::{assert_body_eq, TestRequest}, }; #[derive(Deserialize, Serialize, Debug, PartialEq)] struct Info { hello: String, counter: i64, } #[actix_rt::test] async fn test_form() { let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((CONTENT_LENGTH, 11)) .set_payload(Bytes::from_static(b"hello=world&counter=123")) .to_http_parts(); let Form(s) = Form::::from_request(&req, &mut pl).await.unwrap(); assert_eq!( s, Info { hello: "world".into(), counter: 123 } ); } fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { match err { UrlencodedError::Overflow { .. } => { matches!(other, UrlencodedError::Overflow { .. }) } UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength), UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType), _ => false, } } #[actix_rt::test] async fn test_urlencoded_error() { let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((CONTENT_LENGTH, "xxxx")) .to_http_parts(); let info = UrlEncoded::::new(&req, &mut pl).await; assert!(eq(info.err().unwrap(), UrlencodedError::UnknownLength)); let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((CONTENT_LENGTH, "1000000")) .to_http_parts(); let info = UrlEncoded::::new(&req, &mut pl).await; assert!(eq( info.err().unwrap(), UrlencodedError::Overflow { size: 0, limit: 0 } )); let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, "text/plain")) .insert_header((CONTENT_LENGTH, 10)) .to_http_parts(); let info = UrlEncoded::::new(&req, &mut pl).await; assert!(eq(info.err().unwrap(), UrlencodedError::ContentType)); } #[actix_rt::test] async fn test_urlencoded() { let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded")) .insert_header((CONTENT_LENGTH, 11)) .set_payload(Bytes::from_static(b"hello=world&counter=123")) .to_http_parts(); let info = UrlEncoded::::new(&req, &mut pl).await.unwrap(); assert_eq!( info, Info { hello: "world".to_owned(), counter: 123 } ); let (req, mut pl) = TestRequest::default() .insert_header(( CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", )) .insert_header((CONTENT_LENGTH, 11)) .set_payload(Bytes::from_static(b"hello=world&counter=123")) .to_http_parts(); let info = UrlEncoded::::new(&req, &mut pl).await.unwrap(); assert_eq!( info, Info { hello: "world".to_owned(), counter: 123 } ); } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); let form = Form(Info { hello: "world".to_string(), counter: 123, }); let res = form.respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/x-www-form-urlencoded") ); assert_body_eq!(res, b"hello=world&counter=123"); } #[actix_rt::test] async fn test_with_config_in_data_wrapper() { let ctype = HeaderValue::from_static("application/x-www-form-urlencoded"); let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, ctype)) .insert_header((CONTENT_LENGTH, HeaderValue::from_static("20"))) .set_payload(Bytes::from_static(b"hello=test&counter=4")) .app_data(web::Data::new(FormConfig::default().limit(10))) .to_http_parts(); let s = Form::::from_request(&req, &mut pl).await; assert!(s.is_err()); let err_str = s.err().unwrap().to_string(); assert!(err_str.starts_with("URL encoded payload is larger")); } } actix-web-4.9.0/src/types/header.rs000064400000000000000000000047571046102023000152670ustar 00000000000000//! For header extractor helper documentation, see [`Header`](crate::types::Header). use std::{fmt, ops}; use actix_utils::future::{ready, Ready}; use crate::{ dev::Payload, error::ParseError, extract::FromRequest, http::header::Header as ParseHeader, HttpRequest, }; /// Extract typed headers from the request. /// /// To extract a header, the inner type `T` must implement the /// [`Header`](crate::http::header::Header) trait. /// /// # Examples /// ``` /// use actix_web::{get, web, http::header}; /// /// #[get("/")] /// async fn index(date: web::Header) -> String { /// format!("Request was sent at {}", date.to_string()) /// } /// ``` #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Header(pub T); impl Header { /// Unwrap into the inner `T` value. pub fn into_inner(self) -> T { self.0 } } impl ops::Deref for Header { type Target = T; fn deref(&self) -> &T { &self.0 } } impl ops::DerefMut for Header { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } impl fmt::Display for Header where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } impl FromRequest for Header where T: ParseHeader, { type Error = ParseError; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match ParseHeader::parse(req) { Ok(header) => ready(Ok(Header(header))), Err(err) => ready(Err(err)), } } } #[cfg(test)] mod tests { use super::*; use crate::{ http::{header, Method}, test::TestRequest, }; #[actix_rt::test] async fn test_header_extract() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) .insert_header((header::ALLOW, header::Allow(vec![Method::GET]))) .to_http_parts(); let s = Header::::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.into_inner().0, mime::APPLICATION_JSON); let s = Header::::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.into_inner().0, vec![Method::GET]); assert!(Header::::from_request(&req, &mut pl) .await .is_err()); } } actix-web-4.9.0/src/types/html.rs000064400000000000000000000031021046102023000147620ustar 00000000000000//! Semantic HTML responder. See [`Html`]. use crate::{ http::{ header::{self, ContentType, TryIntoHeaderValue}, StatusCode, }, HttpRequest, HttpResponse, Responder, }; /// Semantic HTML responder. /// /// When used as a responder, creates a 200 OK response, sets the correct HTML content type, and /// uses the string passed to [`Html::new()`] as the body. /// /// ``` /// # use actix_web::web::Html; /// Html::new("

Hello, World!

") /// # ; /// ``` #[derive(Debug, Clone, PartialEq, Hash)] pub struct Html(String); impl Html { /// Constructs a new `Html` responder. pub fn new(html: impl Into) -> Self { Self(html.into()) } } impl Responder for Html { type Body = String; fn respond_to(self, _req: &HttpRequest) -> HttpResponse { let mut res = HttpResponse::with_body(StatusCode::OK, self.0); res.headers_mut().insert( header::CONTENT_TYPE, ContentType::html().try_into_value().unwrap(), ); res } } #[cfg(test)] mod tests { use super::*; use crate::test::TestRequest; #[test] fn responder() { let req = TestRequest::default().to_http_request(); let res = Html::new("

Hello, World!

"); let res = res.respond_to(&req); assert!(res.status().is_success()); assert!(res .headers() .get(header::CONTENT_TYPE) .unwrap() .to_str() .unwrap() .starts_with("text/html")); assert!(res.body().starts_with("

")); } } actix-web-4.9.0/src/types/json.rs000064400000000000000000000600431046102023000147760ustar 00000000000000//! For JSON helper documentation, see [`Json`]. use std::{ fmt, future::Future, marker::PhantomData, ops, pin::Pin, sync::Arc, task::{Context, Poll}, }; use actix_http::Payload; use bytes::BytesMut; use futures_core::{ready, Stream as _}; use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ body::EitherBody, error::{Error, JsonPayloadError}, extract::FromRequest, http::header::{ContentLength, Header as _}, request::HttpRequest, web, HttpMessage, HttpResponse, Responder, }; /// JSON extractor and responder. /// /// `Json` has two uses: JSON responses, and extracting typed data from JSON request payloads. /// /// # Extractor /// To extract typed data from a request body, the inner type `T` must implement the /// [`serde::Deserialize`] trait. /// /// Use [`JsonConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web, App}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// deserialize `Info` from request's body /// #[post("/")] /// async fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// ``` /// /// # Responder /// The `Json` type JSON formatted responses. A handler may return a value of type /// `Json` where `T` is the type of a structure to serialize into JSON. The type `T` must /// implement [`serde::Serialize`]. /// /// ``` /// use actix_web::{post, web, HttpRequest}; /// use serde::Serialize; /// /// #[derive(Serialize)] /// struct Info { /// name: String, /// } /// /// #[post("/{name}")] /// async fn index(req: HttpRequest) -> web::Json { /// web::Json(Info { /// name: req.match_info().get("name").unwrap().to_owned(), /// }) /// } /// ``` #[derive(Debug)] pub struct Json(pub T); impl Json { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } } impl ops::Deref for Json { type Target = T; fn deref(&self) -> &T { &self.0 } } impl ops::DerefMut for Json { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } impl fmt::Display for Json { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } impl Serialize for Json { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.serialize(serializer) } } /// Creates response with OK status code, correct content type header, and serialized JSON payload. /// /// If serialization failed impl Responder for Json { type Body = EitherBody; fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_json::to_string(&self.0) { Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) .message_body(body) { Ok(res) => res.map_into_left_body(), Err(err) => HttpResponse::from_error(err).map_into_right_body(), }, Err(err) => { HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() } } } } /// See [here](#extractor) for example of usage as an extractor. impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let config = JsonConfig::from_req(req); let limit = config.limit; let ctype_required = config.content_type_required; let ctype_fn = config.content_type.as_deref(); let err_handler = config.err_handler.clone(); JsonExtractFut { req: Some(req.clone()), fut: JsonBody::new(req, payload, ctype_fn, ctype_required).limit(limit), err_handler, } } } type JsonErrorHandler = Option Error + Send + Sync>>; pub struct JsonExtractFut { req: Option, fut: JsonBody, err_handler: JsonErrorHandler, } impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); let res = ready!(Pin::new(&mut this.fut).poll(cx)); let res = match res { Err(err) => { let req = this.req.take().unwrap(); log::debug!( "Failed to deserialize Json from payload. \ Request path: {}", req.path() ); if let Some(err_handler) = this.err_handler.as_ref() { Err((*err_handler)(err, &req)) } else { Err(err.into()) } } Ok(data) => Ok(Json(data)), }; Poll::Ready(res) } } /// `Json` extractor configuration. /// /// # Examples /// ``` /// use actix_web::{error, post, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// name: String, /// } /// /// // `Json` extraction is bound by custom `JsonConfig` applied to App. /// #[post("/")] /// async fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.name) /// } /// /// // custom `Json` extractor configuration /// let json_cfg = web::JsonConfig::default() /// // limit request payload size /// .limit(4096) /// // only accept text/plain content type /// .content_type(|mime| mime == mime::TEXT_PLAIN) /// // use custom error handler /// .error_handler(|err, req| { /// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() /// }); /// /// App::new() /// .app_data(json_cfg) /// .service(index); /// ``` #[derive(Clone)] pub struct JsonConfig { limit: usize, err_handler: JsonErrorHandler, content_type: Option bool + Send + Sync>>, content_type_required: bool, } impl JsonConfig { /// Set maximum accepted payload size. By default this limit is 2MB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(JsonPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } /// Set predicate for allowed content types. pub fn content_type(mut self, predicate: F) -> Self where F: Fn(mime::Mime) -> bool + Send + Sync + 'static, { self.content_type = Some(Arc::new(predicate)); self } /// Sets whether or not the request must have a `Content-Type` header to be parsed. pub fn content_type_required(mut self, content_type_required: bool) -> Self { self.content_type_required = content_type_required; self } /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } } const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb /// Allow shared refs used as default. const DEFAULT_CONFIG: JsonConfig = JsonConfig { limit: DEFAULT_LIMIT, err_handler: None, content_type: None, content_type_required: true, }; impl Default for JsonConfig { fn default() -> Self { DEFAULT_CONFIG } } /// Future that resolves to some `T` when parsed from a JSON payload. /// /// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize]. /// /// Returns error if: /// - `Content-Type` is not `application/json` when `ctype_required` (passed to [`new`][Self::new]) /// is `true`. /// - `Content-Length` is greater than [limit](JsonBody::limit()). /// - The payload, when consumed, is not valid JSON. pub enum JsonBody { Error(Option), Body { limit: usize, /// Length as reported by `Content-Length` header, if present. length: Option, #[cfg(feature = "__compress")] payload: Decompress, #[cfg(not(feature = "__compress"))] payload: Payload, buf: BytesMut, _res: PhantomData, }, } impl Unpin for JsonBody {} impl JsonBody { /// Create a new future to decode a JSON request payload. #[allow(clippy::borrow_interior_mutable_const)] pub fn new( req: &HttpRequest, payload: &mut Payload, ctype_fn: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, ctype_required: bool, ) -> Self { // check content-type let can_parse_json = match (ctype_required, req.mime_type()) { (true, Ok(Some(mime))) => { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) || ctype_fn.map_or(false, |predicate| predicate(mime)) } // if content-type is expected but not parsable as mime type, bail (true, _) => false, // if content-type validation is disabled, assume payload is JSON // even when content-type header is missing or invalid mime type (false, _) => true, }; if !can_parse_json { return JsonBody::Error(Some(JsonPayloadError::ContentType)); } let length = ContentLength::parse(req).ok().map(|x| x.0); // Notice the content-length is not checked against limit of json config here. // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. let payload = { cfg_if::cfg_if! { if #[cfg(feature = "__compress")] { Decompress::from_headers(payload.take(), req.headers()) } else { payload.take() } } }; JsonBody::Body { limit: DEFAULT_LIMIT, length, payload, buf: BytesMut::with_capacity(8192), _res: PhantomData, } } /// Set maximum accepted payload size. The default limit is 2MB. pub fn limit(self, limit: usize) -> Self { match self { JsonBody::Body { length, payload, buf, .. } => { if let Some(len) = length { if len > limit { return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength { length: len, limit, })); } } JsonBody::Body { limit, length, payload, buf, _res: PhantomData, } } JsonBody::Error(e) => JsonBody::Error(e), } } } impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); match this { JsonBody::Body { limit, buf, payload, .. } => loop { let res = ready!(Pin::new(&mut *payload).poll_next(cx)); match res { Some(chunk) => { let chunk = chunk?; let buf_len = buf.len() + chunk.len(); if buf_len > *limit { return Poll::Ready(Err(JsonPayloadError::Overflow { limit: *limit })); } else { buf.extend_from_slice(&chunk); } } None => { let json = serde_json::from_slice::(buf) .map_err(JsonPayloadError::Deserialize)?; return Poll::Ready(Ok(json)); } } }, JsonBody::Error(e) => Poll::Ready(Err(e.take().unwrap())), } } } #[cfg(test)] mod tests { use bytes::Bytes; use serde::{Deserialize, Serialize}; use super::*; use crate::{ body, error::InternalError, http::{ header::{self, CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, }, test::{assert_body_eq, TestRequest}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] struct MyObject { name: String, } fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { JsonPayloadError::Overflow { .. } => { matches!(other, JsonPayloadError::Overflow { .. }) } JsonPayloadError::OverflowKnownLength { .. } => { matches!(other, JsonPayloadError::OverflowKnownLength { .. }) } JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); let j = Json(MyObject { name: "test".to_string(), }); let res = j.respond_to(&req); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_TYPE).unwrap(), header::HeaderValue::from_static("application/json") ); assert_body_eq!(res, b"{\"name\":\"test\"}"); } #[actix_rt::test] async fn test_custom_error_responder() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().limit(10).error_handler(|err, _| { let msg = MyObject { name: "invalid request".to_string(), }; let resp = HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); InternalError::from_response(err, resp).into() })) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; let resp = HttpResponse::from_error(s.unwrap_err()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let body = body::to_bytes(resp.into_body()).await.unwrap(); let msg: MyObject = serde_json::from_slice(&body).unwrap(); assert_eq!(msg.name, "invalid request"); } #[actix_rt::test] async fn test_extract() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s.name, "test"); assert_eq!( s.into_inner(), MyObject { name: "test".to_string() } ); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().limit(10)) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(format!("{}", s.err().unwrap()) .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data( JsonConfig::default() .limit(10) .error_handler(|_, _| JsonPayloadError::ContentType.into()), ) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(format!("{}", s.err().unwrap()).contains("Content type error")); } #[actix_rt::test] async fn test_json_body() { let (req, mut pl) = TestRequest::default().to_http_parts(); let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), )) .to_http_parts(); let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("10000"), )) .to_http_parts(); let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; assert!(json_eq( json.err().unwrap(), JsonPayloadError::OverflowKnownLength { length: 10000, limit: 100 } )); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .set_payload(Bytes::from_static(&[0u8; 1000])) .to_http_parts(); let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; assert!(json_eq( json.err().unwrap(), JsonPayloadError::Overflow { limit: 100 } )); let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_http_parts(); let json = JsonBody::::new(&req, &mut pl, None, true).await; assert_eq!( json.ok().unwrap(), MyObject { name: "test".to_owned() } ); } #[actix_rt::test] async fn test_with_json_and_bad_content_type() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().limit(4096)) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_err()) } #[actix_rt::test] async fn test_with_json_and_good_custom_content_type() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().content_type(|mime: mime::Mime| { mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN })) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_ok()) } #[actix_rt::test] async fn test_with_json_and_bad_custom_content_type() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("text/html"), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().content_type(|mime: mime::Mime| { mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN })) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_err()) } #[actix_rt::test] async fn test_json_with_no_content_type() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().content_type_required(false)) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_ok()) } #[actix_rt::test] async fn test_json_ignoring_content_type() { let (req, mut pl) = TestRequest::default() .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), )) .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("invalid/value"), )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(JsonConfig::default().content_type_required(false)) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_ok()); } #[actix_rt::test] async fn test_with_config_in_data_wrapper() { let (req, mut pl) = TestRequest::default() .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)) .insert_header((CONTENT_LENGTH, 16)) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .app_data(web::Data::new(JsonConfig::default().limit(10))) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_err()); let err_str = s.err().unwrap().to_string(); assert!( err_str.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).") ); } } actix-web-4.9.0/src/types/mod.rs000064400000000000000000000006541046102023000146060ustar 00000000000000//! Common extractors and responders. mod either; mod form; mod header; mod html; mod json; mod path; mod payload; mod query; mod readlines; pub use self::{ either::Either, form::{Form, FormConfig, UrlEncoded}, header::Header, html::Html, json::{Json, JsonBody, JsonConfig}, path::{Path, PathConfig}, payload::{Payload, PayloadConfig}, query::{Query, QueryConfig}, readlines::Readlines, }; actix-web-4.9.0/src/types/path.rs000064400000000000000000000214501046102023000147600ustar 00000000000000//! For path segment extractor documentation, see [`Path`]. use std::sync::Arc; use actix_router::PathDeserializer; use actix_utils::future::{ready, Ready}; use derive_more::{AsRef, Deref, DerefMut, Display, From}; use serde::de; use crate::{ dev::Payload, error::{Error, ErrorNotFound, PathError}, web::Data, FromRequest, HttpRequest, }; /// Extract typed data from request path segments. /// /// Use [`PathConfig`] to configure extraction option. /// /// Unlike, [`HttpRequest::match_info`], this extractor will fully percent-decode dynamic segments, /// including `/`, `%`, and `+`. /// /// # Examples /// ``` /// use actix_web::{get, web}; /// /// // extract path info from "/{name}/{count}/index.html" into tuple /// // {name} - deserialize a String /// // {count} - deserialize a u32 /// #[get("/{name}/{count}/index.html")] /// async fn index(path: web::Path<(String, u32)>) -> String { /// let (name, count) = path.into_inner(); /// format!("Welcome {}! {}", name, count) /// } /// ``` /// /// Path segments also can be deserialized into any type that implements [`serde::Deserialize`]. /// Path segment labels will be matched with struct field names. /// /// ``` /// use actix_web::{get, web}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// name: String, /// } /// /// // extract `Info` from a path using serde /// #[get("/{name}")] /// async fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.name) /// } /// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)] pub struct Path(T); impl Path { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } } /// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Path where T: de::DeserializeOwned, { type Error = Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() .or_else(|| req.app_data::>().map(Data::get_ref)) .and_then(|c| c.err_handler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) .map(Path) .map_err(move |err| { log::debug!( "Failed during Path extractor deserialization. \ Request path: {:?}", req.path() ); if let Some(error_handler) = error_handler { let e = PathError::Deserialize(err); (error_handler)(e, req) } else { ErrorNotFound(err) } }), ) } } /// Path extractor configuration /// /// ``` /// use actix_web::web::PathConfig; /// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; /// /// #[derive(Deserialize, Debug)] /// enum Folder { /// #[serde(rename = "inbox")] /// Inbox, /// /// #[serde(rename = "outbox")] /// Outbox, /// } /// /// // deserialize `Info` from request's path /// async fn index(folder: web::Path) -> String { /// format!("Selected folder: {:?}!", folder) /// } /// /// let app = App::new().service( /// web::resource("/messages/{folder}") /// .app_data(PathConfig::default().error_handler(|err, req| { /// error::InternalError::from_response( /// err, /// HttpResponse::Conflict().into(), /// ) /// .into() /// })) /// .route(web::post().to(index)), /// ); /// ``` #[derive(Clone, Default)] pub struct PathConfig { #[allow(clippy::type_complexity)] err_handler: Option Error + Send + Sync>>, } impl PathConfig { /// Set custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } } #[cfg(test)] mod tests { use actix_router::ResourceDef; use derive_more::Display; use serde::Deserialize; use super::*; use crate::{error, http, test::TestRequest, HttpResponse}; #[derive(Deserialize, Debug, Display)] #[display(fmt = "MyStruct({}, {})", key, value)] struct MyStruct { key: String, value: String, } #[derive(Deserialize)] struct Test2 { key: String, value: u32, } #[actix_rt::test] async fn test_extract_path_single() { let resource = ResourceDef::new("/{value}/"); let mut req = TestRequest::with_uri("/32/").to_srv_request(); resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); assert_eq!(*Path::::from_request(&req, &mut pl).await.unwrap(), 32); assert!(Path::::from_request(&req, &mut pl).await.is_err()); } #[allow(clippy::let_unit_value)] #[actix_rt::test] async fn test_tuple_extract() { let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(res.0, "name"); assert_eq!(res.1, "user1"); let (Path(a), Path(b)) = <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(a.0, "name"); assert_eq!(a.1, "user1"); assert_eq!(b.0, "name"); assert_eq!(b.1, "user1"); let () = <()>::from_request(&req, &mut pl).await.unwrap(); } #[actix_rt::test] async fn test_request_extract() { let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let mut s = Path::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s.key, "name"); assert_eq!(s.value, "user1"); s.value = "user2".to_string(); assert_eq!(s.value, "user2"); assert_eq!( format!("{}, {:?}", s, s), "MyStruct(name, user2), Path(MyStruct { key: \"name\", value: \"user2\" })" ); let s = s.into_inner(); assert_eq!(s.value, "user2"); let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); assert_eq!(s.1, "user1"); let mut req = TestRequest::with_uri("/name/32/").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let s = Path::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s.as_ref().key, "name"); assert_eq!(s.value, 32); let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); assert_eq!(s.1, 32); let res = Path::>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(res[0], "name".to_owned()); assert_eq!(res[1], "32".to_owned()); } #[actix_rt::test] async fn paths_decoded() { let resource = ResourceDef::new("/{key}/{value}"); let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request(); resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let path_items = Path::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(path_items.key, "na+me"); assert_eq!(path_items.value, "us/er%42"); assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542"); } #[actix_rt::test] async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") .app_data(PathConfig::default().error_handler(|err, _| { error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() })) .to_http_parts(); let s = Path::<(usize,)>::from_request(&req, &mut pl) .await .unwrap_err(); let res = HttpResponse::from_error(s); assert_eq!(res.status(), http::StatusCode::CONFLICT); } } actix-web-4.9.0/src/types/payload.rs000064400000000000000000000515731046102023000154660ustar 00000000000000//! Basic binary and string payload extractors. use std::{ borrow::Cow, future::Future, pin::Pin, str, task::{Context, Poll}, }; use actix_http::error::PayloadError; use actix_utils::future::{ready, Either, Ready}; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; use futures_core::{ready, stream::Stream}; use mime::Mime; use crate::{ body, dev, error::ErrorBadRequest, http::header, web, Error, FromRequest, HttpMessage, HttpRequest, }; /// Extract a request's raw payload stream. /// /// See [`PayloadConfig`] for important notes when using this advanced extractor. /// /// # Examples /// ``` /// use std::future::Future; /// use futures_util::StreamExt as _; /// use actix_web::{post, web}; /// /// // `body: web::Payload` parameter extracts raw payload stream from request /// #[post("/")] /// async fn index(mut body: web::Payload) -> actix_web::Result { /// // for demonstration only; in a normal case use the `Bytes` extractor /// // collect payload stream into a bytes object /// let mut bytes = web::BytesMut::new(); /// while let Some(item) = body.next().await { /// bytes.extend_from_slice(&item?); /// } /// /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` pub struct Payload(dev::Payload); impl Payload { /// Unwrap to inner Payload type. #[inline] pub fn into_inner(self) -> dev::Payload { self.0 } /// Buffers payload from request up to `limit` bytes. /// /// This method is preferred over [`Payload::to_bytes()`] since it will not lead to unexpected /// memory exhaustion from massive payloads. Note that the other primitive extractors such as /// [`Bytes`] and [`String`], as well as extractors built on top of them, already have this sort /// of protection according to the configured (or default) [`PayloadConfig`]. /// /// # Errors /// /// - The outer error type, [`BodyLimitExceeded`](body::BodyLimitExceeded), is returned when the /// payload is larger than `limit`. /// - The inner error type is [the normal Actix Web error](crate::Error) and is only returned if /// the payload stream yields an error for some reason. Such cases are usually caused by /// unrecoverable connection issues. /// /// # Examples /// /// ``` /// use actix_web::{error, web::Payload, Responder}; /// /// async fn limited_payload_handler(pl: Payload) -> actix_web::Result { /// match pl.to_bytes_limited(5).await { /// Ok(res) => res, /// Err(err) => Err(error::ErrorPayloadTooLarge(err)), /// } /// } /// ``` pub async fn to_bytes_limited( self, limit: usize, ) -> Result, body::BodyLimitExceeded> { let stream = body::BodyStream::new(self.0); match body::to_bytes_limited(stream, limit).await { Ok(Ok(body)) => Ok(Ok(body)), Ok(Err(err)) => Ok(Err(err.into())), Err(err) => Err(err), } } /// Buffers entire payload from request. /// /// Use of this method is discouraged unless you know for certain that requests will not be /// large enough to exhaust memory. If this is not known, prefer [`Payload::to_bytes_limited()`] /// or one of the higher level extractors like [`Bytes`] or [`String`] that implement size /// limits according to the configured (or default) [`PayloadConfig`]. /// /// # Errors /// /// An error is only returned if the payload stream yields an error for some reason. Such cases /// are usually caused by unrecoverable connection issues. /// /// # Examples /// /// ``` /// use actix_web::{error, web::Payload, Responder}; /// /// async fn payload_handler(pl: Payload) -> actix_web::Result { /// pl.to_bytes().await /// } /// ``` pub async fn to_bytes(self) -> crate::Result { let stream = body::BodyStream::new(self.0); Ok(body::to_bytes(stream).await?) } } impl Stream for Payload { type Item = Result; #[inline] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_next(cx) } } /// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Payload { type Error = Error; type Future = Ready>; #[inline] fn from_request(_: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { ready(Ok(Payload(payload.take()))) } } /// Extract binary data from a request's payload. /// /// Collects request payload stream into a [Bytes] instance. /// /// Use [`PayloadConfig`] to configure extraction process. /// /// # Examples /// ``` /// use actix_web::{post, web}; /// /// /// extract binary data from request /// #[post("/")] /// async fn index(body: web::Bytes) -> String { /// format!("Body {:?}!", body) /// } /// ``` impl FromRequest for Bytes { type Error = Error; type Future = Either>>; #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { // allow both Config and Data let cfg = PayloadConfig::from_req(req); if let Err(err) = cfg.check_mimetype(req) { return Either::right(ready(Err(err))); } Either::left(BytesExtractFut { body_fut: HttpMessageBody::new(req, payload).limit(cfg.limit), }) } } /// Future for `Bytes` extractor. pub struct BytesExtractFut { body_fut: HttpMessageBody, } impl Future for BytesExtractFut { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.body_fut).poll(cx).map_err(Into::into) } } /// Extract text information from a request's body. /// /// Text extractor automatically decode body according to the request's charset. /// /// Use [`PayloadConfig`] to configure extraction process. /// /// # Examples /// ``` /// use actix_web::{post, web, FromRequest}; /// /// // extract text data from request /// #[post("/")] /// async fn index(text: String) -> String { /// format!("Body {}!", text) /// } impl FromRequest for String { type Error = Error; type Future = Either>>; #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { let cfg = PayloadConfig::from_req(req); // check content-type if let Err(err) = cfg.check_mimetype(req) { return Either::right(ready(Err(err))); } // check charset let encoding = match req.encoding() { Ok(enc) => enc, Err(err) => return Either::right(ready(Err(err.into()))), }; let limit = cfg.limit; let body_fut = HttpMessageBody::new(req, payload).limit(limit); Either::left(StringExtractFut { body_fut, encoding }) } } /// Future for `String` extractor. pub struct StringExtractFut { body_fut: HttpMessageBody, encoding: &'static Encoding, } impl Future for StringExtractFut { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let encoding = self.encoding; Pin::new(&mut self.body_fut).poll(cx).map(|out| { let body = out?; bytes_to_string(body, encoding) }) } } fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result { if encoding == UTF_8 { Ok(str::from_utf8(body.as_ref()) .map_err(|_| ErrorBadRequest("Can not decode body"))? .to_owned()) } else { Ok(encoding .decode_without_bom_handling_and_without_replacement(&body) .map(Cow::into_owned) .ok_or_else(|| ErrorBadRequest("Can not decode body"))?) } } /// Configuration for request payloads. /// /// Applies to the built-in [`Bytes`] and [`String`] extractors. /// Note that the [`Payload`] extractor does not automatically check /// conformance with this configuration to allow more flexibility when /// building extractors on top of [`Payload`]. /// /// By default, the payload size limit is 256kB and there is no mime type condition. /// /// To use this, add an instance of it to your [`app`](crate::App), [`scope`](crate::Scope) /// or [`resource`](crate::Resource) through the associated `.app_data()` method. #[derive(Clone)] pub struct PayloadConfig { limit: usize, mimetype: Option, } impl PayloadConfig { /// Create new instance with a size limit (in bytes) and no mime type condition. pub fn new(limit: usize) -> Self { Self { limit, ..Default::default() } } /// Set maximum accepted payload size in bytes. The default limit is 256KiB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set required mime type of the request. By default mime type is not enforced. pub fn mimetype(mut self, mt: Mime) -> Self { self.mimetype = Some(mt); self } fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { Ok(Some(ref req_mt)) => { if mt != req_mt { return Err(ErrorBadRequest("Unexpected Content-Type")); } } Ok(None) => { return Err(ErrorBadRequest("Content-Type is expected")); } Err(err) => { return Err(err.into()); } } } Ok(()) } /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config if neither is found. fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) .unwrap_or(&DEFAULT_CONFIG) } } const DEFAULT_CONFIG_LIMIT: usize = 262_144; // 2^18 bytes (~256kB) /// Allow shared refs used as defaults. const DEFAULT_CONFIG: PayloadConfig = PayloadConfig { limit: DEFAULT_CONFIG_LIMIT, mimetype: None, }; impl Default for PayloadConfig { fn default() -> Self { DEFAULT_CONFIG } } /// Future that resolves to a complete HTTP body payload. /// /// By default only 256kB payload is accepted before `PayloadError::Overflow` is returned. /// Use `MessageBody::limit()` method to change upper limit. pub struct HttpMessageBody { limit: usize, length: Option, #[cfg(feature = "__compress")] stream: dev::Decompress, #[cfg(not(feature = "__compress"))] stream: dev::Payload, buf: BytesMut, err: Option, } impl HttpMessageBody { /// Create `MessageBody` for request. #[allow(clippy::borrow_interior_mutable_const)] pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody { let mut length = None; let mut err = None; if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { match l.to_str() { Ok(s) => match s.parse::() { Ok(l) => { if l > DEFAULT_CONFIG_LIMIT { err = Some(PayloadError::Overflow); } length = Some(l) } Err(_) => err = Some(PayloadError::UnknownLength), }, Err(_) => err = Some(PayloadError::UnknownLength), } } let stream = { cfg_if::cfg_if! { if #[cfg(feature = "__compress")] { dev::Decompress::from_headers(payload.take(), req.headers()) } else { payload.take() } } }; HttpMessageBody { stream, limit: DEFAULT_CONFIG_LIMIT, length, buf: BytesMut::with_capacity(8192), err, } } /// Change max size of payload. By default max size is 256kB pub fn limit(mut self, limit: usize) -> Self { if let Some(l) = self.length { self.err = if l > limit { Some(PayloadError::Overflow) } else { None }; } self.limit = limit; self } } impl Future for HttpMessageBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); if let Some(err) = this.err.take() { return Poll::Ready(Err(err)); } loop { let res = ready!(Pin::new(&mut this.stream).poll_next(cx)); match res { Some(chunk) => { let chunk = chunk?; if this.buf.len() + chunk.len() > this.limit { return Poll::Ready(Err(PayloadError::Overflow)); } else { this.buf.extend_from_slice(&chunk); } } None => return Poll::Ready(Ok(this.buf.split().freeze())), } } } } #[cfg(test)] mod tests { use super::*; use crate::{ http::StatusCode, test::{call_service, init_service, read_body, TestRequest}, App, Responder, }; #[actix_rt::test] async fn payload_to_bytes() { async fn payload_handler(pl: Payload) -> crate::Result { pl.to_bytes().await } async fn limited_payload_handler(pl: Payload) -> crate::Result { match pl.to_bytes_limited(5).await { Ok(res) => res, Err(_limited) => Err(ErrorBadRequest("too big")), } } let srv = init_service( App::new() .route("/all", web::to(payload_handler)) .route("limited", web::to(limited_payload_handler)), ) .await; let req = TestRequest::with_uri("/all") .set_payload("1234567890") .to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let body = read_body(res).await; assert_eq!(body, "1234567890"); let req = TestRequest::with_uri("/limited") .set_payload("1234567890") .to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/limited") .set_payload("12345") .to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let body = read_body(res).await; assert_eq!(body, "12345"); } #[actix_rt::test] async fn test_payload_config() { let req = TestRequest::default().to_http_request(); let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); assert!(cfg.check_mimetype(&req).is_err()); let req = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) .to_http_request(); assert!(cfg.check_mimetype(&req).is_err()); let req = TestRequest::default() .insert_header((header::CONTENT_TYPE, "application/json")) .to_http_request(); assert!(cfg.check_mimetype(&req).is_ok()); } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_config_recall_locations() { async fn bytes_handler(_: Bytes) -> impl Responder { "payload is probably json bytes" } async fn string_handler(_: String) -> impl Responder { "payload is probably json string" } let srv = init_service( App::new() .service( web::resource("/bytes-app-data") .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(bytes_handler)), ) .service( web::resource("/bytes-data") .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(bytes_handler)), ) .service( web::resource("/string-app-data") .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(string_handler)), ) .service( web::resource("/string-data") .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(string_handler)), ), ) .await; let req = TestRequest::with_uri("/bytes-app-data").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/bytes-data").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/string-app-data").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/string-data").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/bytes-app-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/bytes-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/string-app-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/string-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_bytes() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_LENGTH, "11")) .set_payload(Bytes::from_static(b"hello=world")) .to_http_parts(); let s = Bytes::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); } #[actix_rt::test] async fn test_string() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_LENGTH, "11")) .set_payload(Bytes::from_static(b"hello=world")) .to_http_parts(); let s = String::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s, "hello=world"); } #[actix_rt::test] async fn test_message_body() { let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_LENGTH, "xxxx")) .to_srv_request() .into_parts(); let res = HttpMessageBody::new(&req, &mut pl).await; match res.err().unwrap() { PayloadError::UnknownLength => {} _ => unreachable!("error"), } let (req, mut pl) = TestRequest::default() .insert_header((header::CONTENT_LENGTH, "1000000")) .to_srv_request() .into_parts(); let res = HttpMessageBody::new(&req, &mut pl).await; match res.err().unwrap() { PayloadError::Overflow => {} _ => unreachable!("error"), } let (req, mut pl) = TestRequest::default() .set_payload(Bytes::from_static(b"test")) .to_http_parts(); let res = HttpMessageBody::new(&req, &mut pl).await; assert_eq!(res.ok().unwrap(), Bytes::from_static(b"test")); let (req, mut pl) = TestRequest::default() .set_payload(Bytes::from_static(b"11111111111111")) .to_http_parts(); let res = HttpMessageBody::new(&req, &mut pl).limit(5).await; match res.err().unwrap() { PayloadError::Overflow => {} _ => unreachable!("error"), } } } actix-web-4.9.0/src/types/query.rs000064400000000000000000000177311046102023000152000ustar 00000000000000//! For query parameter extractor documentation, see [`Query`]. use std::{fmt, ops, sync::Arc}; use actix_utils::future::{err, ok, Ready}; use serde::de::DeserializeOwned; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; /// Extract typed information from the request's query. /// /// To extract typed data from the URL query string, the inner type `T` must implement the /// [`DeserializeOwned`] trait. /// /// Use [`QueryConfig`] to configure extraction process. /// /// # Panics /// A query string consists of unordered `key=value` pairs, therefore it cannot be decoded into any /// type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. /// /// # Examples /// ``` /// use actix_web::{get, web}; /// use serde::Deserialize; /// /// #[derive(Debug, Deserialize)] /// pub enum ResponseType { /// Token, /// Code /// } /// /// #[derive(Debug, Deserialize)] /// pub struct AuthRequest { /// id: u64, /// response_type: ResponseType, /// } /// /// // Deserialize `AuthRequest` struct from query string. /// // This handler gets called only if the request's query parameters contain both fields. /// // A valid request path for this handler would be `/?id=64&response_type=Code"`. /// #[get("/")] /// async fn index(info: web::Query) -> String { /// format!("Authorization request for id={} and type={:?}!", info.id, info.response_type) /// } /// /// // To access the entire underlying query struct, use `.into_inner()`. /// #[get("/debug1")] /// async fn debug1(info: web::Query) -> String { /// dbg!("Authorization object = {:?}", info.into_inner()); /// "OK".to_string() /// } /// /// // Or use destructuring, which is equivalent to `.into_inner()`. /// #[get("/debug2")] /// async fn debug2(web::Query(info): web::Query) -> String { /// dbg!("Authorization object = {:?}", info); /// "OK".to_string() /// } /// ``` #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Query(pub T); impl Query { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } } impl Query { /// Deserialize a `T` from the URL encoded query parameter string. /// /// ``` /// # use std::collections::HashMap; /// # use actix_web::web::Query; /// let numbers = Query::>::from_query("one=1&two=2").unwrap(); /// assert_eq!(numbers.get("one"), Some(&1)); /// assert_eq!(numbers.get("two"), Some(&2)); /// assert!(numbers.get("three").is_none()); /// ``` pub fn from_query(query_str: &str) -> Result { serde_urlencoded::from_str::(query_str) .map(Self) .map_err(QueryPayloadError::Deserialize) } } impl ops::Deref for Query { type Target = T; fn deref(&self) -> &T { &self.0 } } impl ops::DerefMut for Query { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Query { type Error = Error; type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) .unwrap_or_else(move |e| { let e = QueryPayloadError::Deserialize(e); log::debug!( "Failed during Query extractor deserialization. \ Request path: {:?}", req.path() ); let e = if let Some(error_handler) = error_handler { (error_handler)(e, req) } else { e.into() }; err(e) }) } } /// Query extractor configuration. /// /// # Examples /// ``` /// use actix_web::{error, get, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// deserialize `Info` from request's querystring /// #[get("/")] /// async fn index(info: web::Query) -> String { /// format!("Welcome {}!", info.username) /// } /// /// // custom `Query` extractor configuration /// let query_cfg = web::QueryConfig::default() /// // use custom error handler /// .error_handler(|err, req| { /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() /// }); /// /// App::new() /// .app_data(query_cfg) /// .service(index); /// ``` #[derive(Clone, Default)] pub struct QueryConfig { #[allow(clippy::type_complexity)] err_handler: Option Error + Send + Sync>>, } impl QueryConfig { /// Set custom error handler pub fn error_handler(mut self, f: F) -> Self where F: Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, { self.err_handler = Some(Arc::new(f)); self } } #[cfg(test)] mod tests { use actix_http::StatusCode; use derive_more::Display; use serde::Deserialize; use super::*; use crate::{error::InternalError, test::TestRequest, HttpResponse}; #[derive(Deserialize, Debug, Display)] struct Id { id: String, } #[actix_rt::test] async fn test_service_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); assert!(Query::::from_query(req.query_string()).is_err()); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let mut s = Query::::from_query(req.query_string()).unwrap(); assert_eq!(s.id, "test"); assert_eq!( format!("{}, {:?}", s, s), "test, Query(Id { id: \"test\" })" ); s.id = "test1".to_string(); let s = s.into_inner(); assert_eq!(s.id, "test1"); } #[actix_rt::test] async fn test_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); let (req, mut pl) = req.into_parts(); assert!(Query::::from_request(&req, &mut pl).await.is_err()); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let (req, mut pl) = req.into_parts(); let mut s = Query::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s.id, "test"); assert_eq!( format!("{}, {:?}", s, s), "test, Query(Id { id: \"test\" })" ); s.id = "test1".to_string(); let s = s.into_inner(); assert_eq!(s.id, "test1"); } #[actix_rt::test] #[should_panic] async fn test_tuple_panic() { let req = TestRequest::with_uri("/?one=1&two=2").to_srv_request(); let (req, mut pl) = req.into_parts(); Query::<(u32, u32)>::from_request(&req, &mut pl) .await .unwrap(); } #[actix_rt::test] async fn test_custom_error_responder() { let req = TestRequest::with_uri("/name/user1/") .app_data(QueryConfig::default().error_handler(|e, _| { let resp = HttpResponse::UnprocessableEntity().finish(); InternalError::from_response(e, resp).into() })) .to_srv_request(); let (req, mut pl) = req.into_parts(); let query = Query::::from_request(&req, &mut pl).await; assert!(query.is_err()); assert_eq!( query .unwrap_err() .as_response_error() .error_response() .status(), StatusCode::UNPROCESSABLE_ENTITY ); } } actix-web-4.9.0/src/types/readlines.rs000064400000000000000000000155101046102023000157720ustar 00000000000000//! For request line reader documentation, see [`Readlines`]. use std::{ borrow::Cow, pin::Pin, str, task::{Context, Poll}, }; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; use futures_core::{ready, stream::Stream}; use crate::{ dev::Payload, error::{PayloadError, ReadlinesError}, HttpMessage, }; /// Stream that reads request line by line. pub struct Readlines { stream: Payload, buf: BytesMut, limit: usize, checked_buff: bool, encoding: &'static Encoding, err: Option, } impl Readlines where T: HttpMessage, T::Stream: Stream> + Unpin, { /// Create a new stream to read request line by line. pub fn new(req: &mut T) -> Self { let encoding = match req.encoding() { Ok(enc) => enc, Err(err) => return Self::err(err.into()), }; Readlines { stream: req.take_payload(), buf: BytesMut::with_capacity(262_144), limit: 262_144, checked_buff: true, err: None, encoding, } } /// Set maximum accepted payload size. The default limit is 256kB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } fn err(err: ReadlinesError) -> Self { Readlines { stream: Payload::None, buf: BytesMut::new(), limit: 262_144, checked_buff: true, encoding: UTF_8, err: Some(err), } } } impl Stream for Readlines where T: HttpMessage, T::Stream: Stream> + Unpin, { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if let Some(err) = this.err.take() { return Poll::Ready(Some(Err(err))); } // check if there is a newline in the buffer if !this.checked_buff { let mut found: Option = None; for (ind, b) in this.buf.iter().enumerate() { if *b == b'\n' { found = Some(ind); break; } } if let Some(ind) = found { // check if line is longer than limit if ind + 1 > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } let line = if this.encoding == UTF_8 { str::from_utf8(&this.buf.split_to(ind + 1)) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { this.encoding .decode_without_bom_handling_and_without_replacement( &this.buf.split_to(ind + 1), ) .map(Cow::into_owned) .ok_or(ReadlinesError::EncodingError)? }; return Poll::Ready(Some(Ok(line))); } this.checked_buff = true; } // poll req for more bytes match ready!(Pin::new(&mut this.stream).poll_next(cx)) { Some(Ok(mut bytes)) => { // check if there is a newline in bytes let mut found: Option = None; for (ind, b) in bytes.iter().enumerate() { if *b == b'\n' { found = Some(ind); break; } } if let Some(ind) = found { // check if line is longer than limit if ind + 1 > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } let line = if this.encoding == UTF_8 { str::from_utf8(&bytes.split_to(ind + 1)) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { this.encoding .decode_without_bom_handling_and_without_replacement( &bytes.split_to(ind + 1), ) .map(Cow::into_owned) .ok_or(ReadlinesError::EncodingError)? }; // extend buffer with rest of the bytes; this.buf.extend_from_slice(&bytes); this.checked_buff = false; return Poll::Ready(Some(Ok(line))); } this.buf.extend_from_slice(&bytes); Poll::Pending } None => { if this.buf.is_empty() { return Poll::Ready(None); } if this.buf.len() > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } let line = if this.encoding == UTF_8 { str::from_utf8(&this.buf) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { this.encoding .decode_without_bom_handling_and_without_replacement(&this.buf) .map(Cow::into_owned) .ok_or(ReadlinesError::EncodingError)? }; this.buf.clear(); Poll::Ready(Some(Ok(line))) } Some(Err(err)) => Poll::Ready(Some(Err(ReadlinesError::from(err)))), } } } #[cfg(test)] mod tests { use futures_util::StreamExt as _; use super::*; use crate::test::TestRequest; #[actix_rt::test] async fn test_readlines() { let mut req = TestRequest::default() .set_payload(Bytes::from_static( b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ industry. Lorem Ipsum has been the industry's standard dummy\n\ Contrary to popular belief, Lorem Ipsum is not simply random text.", )) .to_request(); let mut stream = Readlines::new(&mut req); assert_eq!( stream.next().await.unwrap().unwrap(), "Lorem Ipsum is simply dummy text of the printing and typesetting\n" ); assert_eq!( stream.next().await.unwrap().unwrap(), "industry. Lorem Ipsum has been the industry's standard dummy\n" ); assert_eq!( stream.next().await.unwrap().unwrap(), "Contrary to popular belief, Lorem Ipsum is not simply random text." ); } } actix-web-4.9.0/src/web.rs000064400000000000000000000151511046102023000134360ustar 00000000000000//! Essentials helper functions and types for application registration. //! //! # Request Extractors //! - [`Data`]: Application data item //! - [`ThinData`]: Cheap-to-clone application data item //! - [`ReqData`]: Request-local data item //! - [`Path`]: URL path parameters / dynamic segments //! - [`Query`]: URL query parameters //! - [`Header`]: Typed header //! - [`Json`]: JSON payload //! - [`Form`]: URL-encoded payload //! - [`Bytes`]: Raw payload //! //! # Responders //! - [`Json`]: JSON response //! - [`Form`]: URL-encoded response //! - [`Bytes`]: Raw bytes response //! - [`Redirect`](Redirect::to): Convenient redirect responses use std::{borrow::Cow, future::Future}; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; pub use crate::{ config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, thin_data::ThinData, types::*, }; use crate::{ error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource, Responder, Route, Scope, }; /// Creates a new resource for a specific path. /// /// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c` /// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. /// /// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used /// later in a request handler to access the matched value for that segment. This is done by looking /// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method. /// /// By default, each segment matches the regular expression `[^{}/]+`. /// /// You can also specify a custom regex in the form `{identifier:regex}`: /// /// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store /// `userid` and `friend` in the exposed `Path` object: /// /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( /// web::resource("/users/{userid}/{friend}") /// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` pub fn resource(path: T) -> Resource { Resource::new(path) } /// Creates scope for common path prefix. /// /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic /// path segments. /// /// # Avoid Trailing Slashes /// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost /// certainly not have the expected behavior. See the [documentation on resource definitions][pat] /// to understand why this is the case and how to correctly construct scope/prefix definitions. /// /// # Examples /// In this example, three routes are set up (and will handle any method): /// - `/{project_id}/path1` /// - `/{project_id}/path2` /// - `/{project_id}/path3` /// /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( /// web::scope("/{project_id}") /// .service(web::resource("/path1").to(|| HttpResponse::Ok())) /// .service(web::resource("/path2").to(|| HttpResponse::Ok())) /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` /// /// [pat]: crate::dev::ResourceDef#prefix-resources pub fn scope(path: &str) -> Scope { Scope::new(path) } /// Creates a new un-configured route. pub fn route() -> Route { Route::new() } macro_rules! method_route { ($method_fn:ident, $method_const:ident) => { #[doc = concat!(" Creates a new route with `", stringify!($method_const), "` method guard.")] /// /// # Examples #[doc = concat!(" In this example, one `", stringify!($method_const), " /{project_id}` route is set up:")] /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( /// web::resource("/{project_id}") #[doc = concat!(" .route(web::", stringify!($method_fn), "().to(|| HttpResponse::Ok()))")] /// /// ); /// ``` pub fn $method_fn() -> Route { method(Method::$method_const) } }; } method_route!(get, GET); method_route!(post, POST); method_route!(put, PUT); method_route!(patch, PATCH); method_route!(delete, DELETE); method_route!(head, HEAD); method_route!(trace, TRACE); /// Creates a new route with specified method guard. /// /// # Examples /// In this example, one `GET /{project_id}` route is set up: /// /// ``` /// use actix_web::{web, http, App, HttpResponse}; /// /// let app = App::new().service( /// web::resource("/{project_id}") /// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) /// ); /// ``` pub fn method(method: Method) -> Route { Route::new().method(method) } /// Creates a new any-method route with handler. /// /// ``` /// use actix_web::{web, App, HttpResponse, Responder}; /// /// async fn index() -> impl Responder { /// HttpResponse::Ok() /// } /// /// App::new().service( /// web::resource("/").route( /// web::to(index)) /// ); /// ``` pub fn to(handler: F) -> Route where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, { Route::new().to(handler) } /// Creates a raw service for a specific path. /// /// ``` /// use actix_web::{dev, web, guard, App, Error, HttpResponse}; /// /// async fn my_service(req: dev::ServiceRequest) -> Result { /// Ok(req.into_response(HttpResponse::Ok().finish())) /// } /// /// let app = App::new().service( /// web::service("/users/*") /// .guard(guard::Header("content-type", "text/plain")) /// .finish(my_service) /// ); /// ``` pub fn service(path: T) -> WebService { WebService::new(path) } /// Create a relative or absolute redirect. /// /// See [`Redirect`] docs for usage details. /// /// # Examples /// ``` /// use actix_web::{web, App}; /// /// let app = App::new() /// // the client will resolve this redirect to /api/to-path /// .service(web::redirect("/api/from-path", "to-path")); /// ``` pub fn redirect(from: impl Into>, to: impl Into>) -> Redirect { Redirect::new(from, to) } /// Executes blocking function on a thread pool, returns future that resolves to result of the /// function execution. pub fn block(f: F) -> impl Future> where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { let fut = actix_rt::task::spawn_blocking(f); async { fut.await.map_err(|_| BlockingError) } } actix-web-4.9.0/tests/compression.rs000064400000000000000000000232621046102023000155770ustar 00000000000000use actix_http::ContentEncoding; use actix_web::{ http::{header, StatusCode}, middleware::Compress, web, App, HttpResponse, }; use bytes::Bytes; mod utils; static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt"); static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz"); static LOREM_BR: &[u8] = include_bytes!("fixtures/lorem.txt.br"); static LOREM_ZSTD: &[u8] = include_bytes!("fixtures/lorem.txt.zst"); static LOREM_XZ: &[u8] = include_bytes!("fixtures/lorem.txt.xz"); macro_rules! test_server { () => { actix_test::start(|| { App::new() .wrap(Compress::default()) .route( "/static", web::to(|| async { HttpResponse::Ok().body(LOREM) }), ) .route( "/static-gzip", web::to(|| async { HttpResponse::Ok() // signal to compressor that content should not be altered // signal to client that content is encoded .insert_header(ContentEncoding::Gzip) .body(LOREM_GZIP) }), ) .route( "/static-br", web::to(|| async { HttpResponse::Ok() // signal to compressor that content should not be altered // signal to client that content is encoded .insert_header(ContentEncoding::Brotli) .body(LOREM_BR) }), ) .route( "/static-zstd", web::to(|| async { HttpResponse::Ok() // signal to compressor that content should not be altered // signal to client that content is encoded .insert_header(ContentEncoding::Zstd) .body(LOREM_ZSTD) }), ) .route( "/static-xz", web::to(|| async { HttpResponse::Ok() // signal to compressor that content should not be altered // signal to client that content is encoded as 7zip .insert_header((header::CONTENT_ENCODING, "xz")) .body(LOREM_XZ) }), ) .route( "/echo", web::to(|body: Bytes| async move { HttpResponse::Ok().body(body) }), ) }) }; } #[actix_rt::test] async fn negotiate_encoding_identity() { let srv = test_server!(); let req = srv .post("/static") .insert_header((header::ACCEPT_ENCODING, "identity")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); srv.stop().await; } #[actix_rt::test] async fn negotiate_encoding_gzip() { let srv = test_server!(); let req = srv .post("/static") .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); let mut res = srv .post("/static") .no_decompress() .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send() .await .unwrap(); let bytes = res.body().await.unwrap(); assert_eq!(utils::gzip::decode(bytes), LOREM); srv.stop().await; } #[actix_rt::test] async fn negotiate_encoding_br() { let srv = test_server!(); // check that brotli content-encoding header is returned let req = srv .post("/static") .insert_header((header::ACCEPT_ENCODING, "br, zstd, gzip")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); // check that brotli is preferred even when later in (q-less) list let req = srv .post("/static") .insert_header((header::ACCEPT_ENCODING, "gzip, zstd, br")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); // check that returned content is actually brotli encoded let mut res = srv .post("/static") .no_decompress() .insert_header((header::ACCEPT_ENCODING, "br, zstd, gzip")) .send() .await .unwrap(); let bytes = res.body().await.unwrap(); assert_eq!(utils::brotli::decode(bytes), LOREM); srv.stop().await; } #[actix_rt::test] async fn negotiate_encoding_zstd() { let srv = test_server!(); let req = srv .post("/static") .insert_header((header::ACCEPT_ENCODING, "zstd, gzip, br;q=0.8")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "zstd"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); let mut res = srv .post("/static") .no_decompress() .insert_header((header::ACCEPT_ENCODING, "zstd, gzip, br;q=0.8")) .send() .await .unwrap(); let bytes = res.body().await.unwrap(); assert_eq!(utils::zstd::decode(bytes), LOREM); srv.stop().await; } #[cfg(all( feature = "compress-brotli", feature = "compress-gzip", feature = "compress-zstd", ))] #[actix_rt::test] async fn client_encoding_prefers_brotli() { let srv = test_server!(); let req = srv.post("/static").send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); srv.stop().await; } #[actix_rt::test] async fn gzip_no_decompress() { let srv = test_server!(); let req = srv .post("/static-gzip") // don't decompress response body .no_decompress() // signal that we want a compressed body .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM_GZIP)); srv.stop().await; } #[actix_rt::test] async fn manual_custom_coding() { let srv = test_server!(); let req = srv .post("/static-xz") // don't decompress response body .no_decompress() // signal that we want a compressed body .insert_header((header::ACCEPT_ENCODING, "xz")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM_XZ)); srv.stop().await; } #[actix_rt::test] async fn deny_identity_coding() { let srv = test_server!(); let req = srv .post("/static") // signal that we want a compressed body .insert_header((header::ACCEPT_ENCODING, "br, identity;q=0")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); srv.stop().await; } #[actix_rt::test] async fn deny_identity_coding_no_decompress() { let srv = test_server!(); let req = srv .post("/static-br") // don't decompress response body .no_decompress() // signal that we want a compressed body .insert_header((header::ACCEPT_ENCODING, "br, identity;q=0")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM_BR)); srv.stop().await; } // TODO: fix test // currently fails because negotiation doesn't consider unknown encoding types #[ignore] #[actix_rt::test] async fn deny_identity_for_manual_coding() { let srv = test_server!(); let req = srv .post("/static-xz") // don't decompress response body .no_decompress() // signal that we want a compressed body .insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0")) .send(); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz"); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM_XZ)); srv.stop().await; } actix-web-4.9.0/tests/fixtures/lorem.txt000064400000000000000000000040111046102023000164070ustar 00000000000000Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin interdum tincidunt lacus, sed tempor lorem consectetur et. Pellentesque et egestas sem, at cursus massa. Nunc feugiat elit sit amet ipsum commodo luctus. Proin auctor dignissim pharetra. Integer iaculis quam a tellus auctor, vitae auctor nisl viverra. Nullam consequat maximus porttitor. Pellentesque tortor enim, molestie at varius non, tempor non nibh. Suspendisse tempus erat lorem, vel faucibus magna blandit vel. Sed pellentesque ligula augue, vitae fermentum eros blandit et. Cras dignissim in massa ut varius. Vestibulum commodo nunc sit amet pellentesque dignissim. Donec imperdiet blandit lobortis. Suspendisse fringilla nunc quis venenatis tempor. Nunc tempor sed erat sed convallis. Pellentesque aliquet elit lectus, quis vulputate arcu pharetra sed. Etiam laoreet aliquet arcu cursus vehicula. Maecenas odio odio, elementum faucibus sollicitudin vitae, pellentesque ac purus. Donec venenatis faucibus lorem, et finibus lacus tincidunt vitae. Quisque laoreet metus sapien, vitae euismod mauris lobortis malesuada. Integer sit amet elementum turpis. Maecenas ex mauris, dapibus eu placerat vitae, rutrum convallis enim. Nulla vitae orci ultricies, sagittis turpis et, lacinia dui. Praesent egestas urna turpis, sit amet feugiat mauris tristique eu. Quisque id tempor libero. Donec ullamcorper dapibus lorem, vel consequat risus congue a. Nullam dignissim ut lectus vitae tempor. Pellentesque ut odio fringilla, volutpat mi et, vulputate tellus. Fusce eget diam non odio tincidunt viverra eu vel augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam sed eleifend purus, vitae aliquam orci. Cras fringilla justo eget tempus bibendum. Phasellus venenatis, odio nec pulvinar commodo, quam neque lacinia turpis, ut rutrum tortor massa eu nulla. Vivamus tincidunt ut lectus a gravida. Donec varius mi quis enim interdum ultrices. Sed aliquam consectetur nisi vitae viverra. Praesent nec ligula egestas, porta lectus sed, consectetur augue. actix-web-4.9.0/tests/fixtures/lorem.txt.br000064400000000000000000000016111046102023000170140ustar 00000000000000@@ jק}Bc- )'ݐ$ݐDti V!Utœc/ugԄL mYhKrHh:;~/ɿk"i,1UlPrqi ҫc,q8i'z5)∘ùYBps2H(3:QVwPD_9uP>:Y]aI U;j/nRn2bMܶ$P5Յ'^LBd*xZ#> U˯N: SMgޡ:n #83ۜ `actix-web-4.9.0/tests/fixtures/lorem.txt.gz000064400000000000000000000016301046102023000170320ustar 00000000000000alorem.txt]UK6 )x/e>@$H6@<4So3Ӷ%XU,g*mHI3!y5$k\"񦧦UN4ԓ,qެ~խLגJQDEWx&V@lO_Ed9agZKL%)*~]L#l\YsI 6ݽ%GTgIi10;cƅYFpx+$P끽9gņ"^UV| qFJ:o/#JĦ*齒{c ۡ^!(1Yd9B?pZ2`_k^܋ ?k>>~ ^Vㄉ .S/םY4VzsVMvUN>K/^Y2gkޱj75+w y u' ¦YptΧ|)ުl*s+%獹YPȗo:~@(DMS<ü1b_}aOc{v/{6c`wKJ]:0}:Wr[baûjz$Yz*`ڊZ$)S"^.#`zg|*az1/Ck0leH@U|nb z\SV{oŢj98'Ok@+[跒V12Ù3ʫ?j4ې3= vڴ66R1ḫ.FIu {^,= Z;=FYZ1YZYS=YKhݕ=[!o>xKB=SmXԶF5eE:j=<̻hV_} f~_] actix-web-4.9.0/tests/fixtures/lorem.txt.xz000064400000000000000000000017741046102023000170640ustar 000000000000007zXZִF!X]&FgZw}A5̓|ې/qr}Vqj"=U̥vk鯲tܹ(\d@ >v򨎐؁Đ/c[[HzuK;z[/QS] |I;\,hm "m2GFùAi25 N ۥ!2HPK3o40$<7=e FE_.MbL@Qn7!/CU|j,ajf<3;ƮՄ6 Έ84R\O.z%5(ڕ,Bj"E_5ZlZU"㎡-~ z5ΤHY S{"4OWLmlui @(9T_l4vu9H8A*tX$-CiH  hX&|πӵEot|mR>,m"1*Ŏ>1\lUL2ӥ(VΞH/d޽CLIS(ta9F_|k$+~5|l9Y GGfFa 2 \&VJrSwΩ`(8q[O(ub^QHgE{||@K15G&"uw˜O"Ϭ۵{fUv v%wq=s,pW1n?id8u opj|ɠ4!$YԤ5\zBCqť)$V>|uPR쌵Wˆe9gYZactix-web-4.9.0/tests/fixtures/lorem.txt.zst000064400000000000000000000016021046102023000172310ustar 00000000000000(/d Fy`M"۱>Ŷ ;&{,cкV{Kg<0 !_XK&$#Del(U f|t^-n![xbgZ Z G8ͬ^BN$1G\[h"u29M]Ō t4;%t97}3LshL$D6'), @w4&sC5lpa2/Wwh=Xyڝ~8~nCrJtPgu`5E%0J<<)bO?,vfgުv*1U%!!{#kC#ڞS"jD+J΂pQ'Uactix-web-4.9.0/tests/test-macro-import-conflict.rs000064400000000000000000000011371046102023000204200ustar 00000000000000//! Checks that test macro does not cause problems in the presence of imports named "test" that //! could be either a module with test items or the "test with runtime" macro itself. //! //! Before actix/actix-net#399 was implemented, this macro was running twice. The first run output //! `#[test]` and it got run again and since it was in scope. //! //! Prevented by using the fully-qualified test marker (`#[::core::prelude::v1::test]`). use actix_web::test; #[actix_web::test] async fn test_macro_naming_conflict() { let _req = test::TestRequest::default(); assert_eq!(async { 1 }.await, 1); } actix-web-4.9.0/tests/test_error_propagation.rs000064400000000000000000000051761046102023000200350ustar 00000000000000use std::sync::Arc; use actix_utils::future::{ok, Ready}; use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, get, test::{call_service, init_service, TestRequest}, ResponseError, }; use futures_core::future::LocalBoxFuture; use futures_util::lock::Mutex; #[derive(Debug, Clone)] pub struct MyError; impl ResponseError for MyError {} impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "A custom error") } } #[get("/test")] async fn test() -> Result { Err(MyError.into()) } #[derive(Clone)] pub struct SpyMiddleware(Arc>>); impl Transform for SpyMiddleware where S: Service, Error = actix_web::Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Transform = Middleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(Middleware { was_error: self.0.clone(), service, }) } } #[doc(hidden)] pub struct Middleware { was_error: Arc>>, service: S, } impl Service for Middleware where S: Service, Error = actix_web::Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Future = LocalBoxFuture<'static, Result>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let lock = self.was_error.clone(); let response_future = self.service.call(req); Box::pin(async move { let response = response_future.await; if let Ok(success) = &response { *lock.lock().await = Some(success.response().error().is_some()); } response }) } } #[actix_rt::test] async fn error_cause_should_be_propagated_to_middlewares() { let lock = Arc::new(Mutex::new(None)); let spy_middleware = SpyMiddleware(lock.clone()); let app = init_service( actix_web::App::new() .wrap(spy_middleware.clone()) .service(test), ) .await; call_service(&app, TestRequest::with_uri("/test").to_request()).await; let was_error_captured = lock.lock().await.unwrap(); assert!(was_error_captured); } actix-web-4.9.0/tests/test_httpserver.rs000064400000000000000000000101371046102023000165000ustar 00000000000000#[cfg(feature = "openssl")] extern crate tls_openssl as openssl; #[cfg(any(unix, feature = "openssl"))] use { actix_web::{web, App, HttpResponse, HttpServer}, std::{sync::mpsc, thread, time::Duration}, }; #[cfg(unix)] #[actix_rt::test] async fn test_start() { let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { actix_rt::System::new() .block_on(async { let srv = HttpServer::new(|| { App::new().service( web::resource("/") .route(web::to(|| async { HttpResponse::Ok().body("test") })), ) }) .workers(1) .backlog(1) .max_connections(10) .max_connection_rate(10) .keep_alive(Duration::from_secs(10)) .client_request_timeout(Duration::from_secs(5)) .client_disconnect_timeout(Duration::ZERO) .server_hostname("localhost") .system_exit() .disable_signals() .bind(format!("{}", addr)) .unwrap() .run(); tx.send(srv.handle()).unwrap(); srv.await }) .unwrap(); }); let srv = rx.recv().unwrap(); let client = awc::Client::builder() .connector(awc::Connector::new().timeout(Duration::from_millis(100))) .finish(); let host = format!("http://{}", addr); let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); srv.stop(false).await; } #[cfg(feature = "openssl")] fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, x509::X509, }; let rcgen::CertifiedKey { cert, key_pair } = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_file = cert.pem(); let key_file = key_pair.serialize_pem(); let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); builder.set_certificate(&cert).unwrap(); builder.set_private_key(&key).unwrap(); builder } #[actix_rt::test] #[cfg(feature = "openssl")] async fn test_start_ssl() { use actix_web::HttpRequest; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { actix_rt::System::new() .block_on(async { let builder = ssl_acceptor(); let srv = HttpServer::new(|| { App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { assert!(req.app_config().secure()); async { HttpResponse::Ok().body("test") } }))) }) .workers(1) .shutdown_timeout(1) .system_exit() .disable_signals() .bind_openssl(format!("{}", addr), builder) .unwrap(); let srv = srv.run(); tx.send(srv.handle()).unwrap(); srv.await }) .unwrap() }); let srv = rx.recv().unwrap(); let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); let _ = builder .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); let client = awc::Client::builder() .connector( awc::Connector::new() .openssl(builder.build()) .timeout(Duration::from_millis(100)), ) .finish(); let host = format!("https://{}", addr); let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); srv.stop(false).await; } actix-web-4.9.0/tests/test_server.rs000064400000000000000000000664221046102023000156100ustar 00000000000000#[cfg(feature = "openssl")] extern crate tls_openssl as openssl; #[cfg(feature = "rustls-0_23")] extern crate tls_rustls as rustls; use std::{ future::Future, io::{Read, Write}, pin::Pin, task::{Context, Poll}, time::Duration, }; use actix_web::{ cookie::Cookie, http::{header, StatusCode}, middleware::{Compress, NormalizePath, TrailingSlash}, web, App, Error, HttpResponse, }; use bytes::Bytes; use futures_core::ready; #[cfg(feature = "openssl")] use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, x509::X509, }; use rand::{distributions::Alphanumeric, Rng as _}; mod utils; const S: &str = "Hello World "; const STR: &str = const_str::repeat!(S, 100); #[cfg(feature = "openssl")] fn openssl_config() -> SslAcceptor { let rcgen::CertifiedKey { cert, key_pair } = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_file = cert.pem(); let key_file = key_pair.serialize_pem(); let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); builder.set_certificate(&cert).unwrap(); builder.set_private_key(&key).unwrap(); builder.set_alpn_select_callback(|_, protos| { const H2: &[u8] = b"\x02h2"; if protos.windows(3).any(|window| window == H2) { Ok(b"h2") } else { Err(openssl::ssl::AlpnError::NOACK) } }); builder.set_alpn_protos(b"\x02h2").unwrap(); builder.build() } struct TestBody { data: Bytes, chunk_size: usize, delay: Pin>, } impl TestBody { fn new(data: Bytes, chunk_size: usize) -> Self { TestBody { data, chunk_size, delay: Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))), } } } impl futures_core::stream::Stream for TestBody { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(Pin::new(&mut self.delay).poll(cx)); self.delay = Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))); let chunk_size = std::cmp::min(self.chunk_size, self.data.len()); let chunk = self.data.split_to(chunk_size); if chunk.is_empty() { Poll::Ready(None) } else { Poll::Ready(Some(Ok(chunk))) } } } #[actix_rt::test] async fn test_body() { let srv = actix_test::start(|| { App::new() .service(web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) }))) }); let mut res = srv.get("/").send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } // enforcing an encoding per-response is removed // #[actix_rt::test] // async fn test_body_encoding_override() { // let srv = actix_test::start_with(actix_test::config().h1(), || { // App::new() // .wrap(Compress::default()) // .service(web::resource("/").route(web::to(|| { // HttpResponse::Ok() // .encode_with(ContentEncoding::Deflate) // .body(STR) // }))) // .service(web::resource("/raw").route(web::to(|| { // let mut res = HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); // res.encode_with(ContentEncoding::Deflate); // res.map_into_boxed_body() // }))) // }); // // Builder // let mut res = srv // .get("/") // .no_decompress() // .append_header((ACCEPT_ENCODING, "deflate")) // .send() // .await // .unwrap(); // assert_eq!(res.status(), StatusCode::OK); // let bytes = res.body().await.unwrap(); // assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); // // Raw Response // let mut res = srv // .request(actix_web::http::Method::GET, srv.url("/raw")) // .no_decompress() // .append_header((ACCEPT_ENCODING, "deflate")) // .send() // .await // .unwrap(); // assert_eq!(res.status(), StatusCode::OK); // let bytes = res.body().await.unwrap(); // assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); // srv.stop().await; // } #[actix_rt::test] async fn body_gzip_large() { let data = STR.repeat(10); let srv_data = data.clone(); let srv = actix_test::start_with(actix_test::config().h1(), move || { let data = srv_data.clone(); App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(move || { let data = data.clone(); async move { HttpResponse::Ok().body(data.clone()) } }))) }); let mut res = srv .get("/") .no_decompress() .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::gzip::decode(bytes), data.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_gzip_large_random() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(70_000) .map(char::from) .collect::(); let srv_data = data.clone(); let srv = actix_test::start_with(actix_test::config().h1(), move || { let data = srv_data.clone(); App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(move || { let data = data.clone(); async move { HttpResponse::Ok().body(data.clone()) } }))) }); let mut res = srv .get("/") .no_decompress() .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::gzip::decode(bytes), data.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_chunked_implicit() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::get().to(|| async { HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); let mut res = srv .get("/") .no_decompress() .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::TRANSFER_ENCODING).unwrap(), "chunked" ); let bytes = res.body().await.unwrap(); assert_eq!(utils::gzip::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_br_streaming() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(|| async { HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); let mut res = srv .get("/") .append_header((header::ACCEPT_ENCODING, "br")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::brotli::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_head_binary() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( web::resource("/") .route(web::head().to(move || async { HttpResponse::Ok().body(STR) })), ) }); let mut res = srv.head("/").send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let len = res.headers().get(header::CONTENT_LENGTH).unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); let bytes = res.body().await.unwrap(); assert!(bytes.is_empty()); srv.stop().await; } #[actix_rt::test] async fn test_no_chunking() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move || async { HttpResponse::Ok() .no_chunking(STR.len() as u64) .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); let mut res = srv.get("/").send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key(header::TRANSFER_ENCODING)); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } #[actix_rt::test] async fn test_body_deflate() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().wrap(Compress::default()).service( web::resource("/").route(web::to(move || async { HttpResponse::Ok().body(STR) })), ) }); let mut res = srv .get("/") .append_header((header::ACCEPT_ENCODING, "deflate")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_brotli() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().wrap(Compress::default()).service( web::resource("/").route(web::to(move || async { HttpResponse::Ok().body(STR) })), ) }); let mut res = srv .get("/") .append_header((header::ACCEPT_ENCODING, "br")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::brotli::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_zstd() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().wrap(Compress::default()).service( web::resource("/").route(web::to(move || async { HttpResponse::Ok().body(STR) })), ) }); let mut res = srv .get("/") .append_header((header::ACCEPT_ENCODING, "zstd")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::zstd::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_body_zstd_streaming() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(move || async { HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); let mut res = srv .get("/") .append_header((header::ACCEPT_ENCODING, "zstd")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(utils::zstd::decode(bytes), STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_zstd_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "zstd")) .send_body(utils::zstd::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } #[actix_rt::test] async fn test_zstd_encoding_large() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(320_000) .map(char::from) .collect::(); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( web::resource("/") .app_data(web::PayloadConfig::new(320_000)) .route(web::to(move |body: Bytes| async { HttpResponse::Ok().streaming(TestBody::new(body, 10240)) })), ) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "zstd")) .send_body(utils::zstd::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().limit(320_000).await.unwrap(); assert_eq!(bytes, data.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .insert_header((header::CONTENT_ENCODING, "gzip")) .send_body(utils::gzip::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } #[actix_rt::test] async fn test_gzip_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "gzip")) .send_body(utils::gzip::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, STR.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_gzip_encoding_large() { let data = STR.repeat(10); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let req = srv .post("/") .append_header((header::CONTENT_ENCODING, "gzip")) .send_body(utils::gzip::encode(&data)); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, data); srv.stop().await; } #[actix_rt::test] async fn test_reading_gzip_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(60_000) .map(char::from) .collect::(); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "gzip")) .send_body(utils::gzip::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, data.as_bytes()); srv.stop().await; } #[actix_rt::test] async fn test_reading_deflate_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "deflate")) .send_body(utils::deflate::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } #[actix_rt::test] async fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "deflate")) .send_body(utils::deflate::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); srv.stop().await; } #[actix_rt::test] async fn test_reading_deflate_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(160_000) .map(char::from) .collect::(); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "deflate")) .send_body(utils::deflate::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); srv.stop().await; } #[actix_rt::test] async fn test_brotli_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move |body: Bytes| async { HttpResponse::Ok().body(body) }))) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "br")) .send_body(utils::brotli::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); srv.stop().await; } #[actix_rt::test] async fn test_brotli_encoding_large() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(320_000) .map(char::from) .collect::(); let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( web::resource("/") .app_data(web::PayloadConfig::new(320_000)) .route(web::to(move |body: Bytes| async { HttpResponse::Ok().streaming(TestBody::new(body, 10240)) })), ) }); let request = srv .post("/") .append_header((header::CONTENT_ENCODING, "br")) .send_body(utils::brotli::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); srv.stop().await; } #[cfg(feature = "openssl")] #[actix_rt::test] async fn test_brotli_encoding_large_openssl() { use actix_web::http::header; let data = STR.repeat(10); let srv = actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { // echo decompressed request body back in response HttpResponse::Ok() .insert_header(header::ContentEncoding::Identity) .body(bytes) }))) }); let mut res = srv .post("/") .append_header((header::CONTENT_ENCODING, "br")) .send_body(utils::brotli::encode(&data)) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); srv.stop().await; } #[cfg(feature = "rustls-0_23")] mod plus_rustls { use std::io::BufReader; use rustls::{pki_types::PrivateKeyDer, ServerConfig as RustlsServerConfig}; use rustls_pemfile::{certs, pkcs8_private_keys}; use super::*; fn tls_config() -> RustlsServerConfig { let rcgen::CertifiedKey { cert, key_pair } = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_file = cert.pem(); let key_file = key_pair.serialize_pem(); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); let cert_chain = certs(cert_file).collect::, _>>().unwrap(); let mut keys = pkcs8_private_keys(key_file) .collect::, _>>() .unwrap(); RustlsServerConfig::builder() .with_no_client_auth() .with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0))) .unwrap() } #[actix_rt::test] async fn test_reading_deflate_encoding_large_random_rustls() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(160_000) .map(char::from) .collect::(); let srv = actix_test::start_with(actix_test::config().rustls_0_23(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { // echo decompressed request body back in response HttpResponse::Ok() .insert_header(header::ContentEncoding::Identity) .body(bytes) }))) }); let req = srv .post("/") .insert_header((header::CONTENT_ENCODING, "deflate")) .send_stream(TestBody::new( Bytes::from(utils::deflate::encode(&data)), 1024, )); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); srv.stop().await; } } #[actix_rt::test] async fn test_server_cookies() { use actix_web::http; let srv = actix_test::start(|| { App::new().default_service(web::to(|| async { HttpResponse::Ok() .cookie( Cookie::build("first", "first_value") .http_only(true) .finish(), ) .cookie(Cookie::new("second", "first_value")) .cookie(Cookie::new("second", "second_value")) .finish() })) }); let req = srv.get("/"); let res = req.send().await.unwrap(); assert!(res.status().is_success()); { let first_cookie = Cookie::build("first", "first_value") .http_only(true) .finish(); let second_cookie = Cookie::new("second", "first_value"); let cookies = res.cookies().expect("To have cookies"); assert_eq!(cookies.len(), 3); if cookies[0] == first_cookie { assert_eq!(cookies[1], second_cookie); } else { assert_eq!(cookies[0], second_cookie); assert_eq!(cookies[1], first_cookie); } let first_cookie = first_cookie.to_string(); let second_cookie = second_cookie.to_string(); // Check that we have exactly two instances of raw cookie headers let cookies = res .headers() .get_all(http::header::SET_COOKIE) .map(|header| header.to_str().expect("To str").to_string()) .collect::>(); assert_eq!(cookies.len(), 3); if cookies[0] == first_cookie { assert_eq!(cookies[1], second_cookie); } else { assert_eq!(cookies[0], second_cookie); assert_eq!(cookies[1], first_cookie); } } srv.stop().await; } #[actix_rt::test] async fn test_slow_request() { use std::net; let srv = actix_test::start_with( actix_test::config().client_request_timeout(Duration::from_millis(200)), || App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), ); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); srv.stop().await; } #[actix_rt::test] async fn test_normalize() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(NormalizePath::new(TrailingSlash::Trim)) .service(web::resource("/one").route(web::to(HttpResponse::Ok))) }); let res = srv.get("/one/").send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); srv.stop().await } // allow deprecated App::data #[allow(deprecated)] #[actix_rt::test] async fn test_data_drop() { use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; struct TestData(Arc); impl TestData { fn new(inner: Arc) -> Self { let _ = inner.fetch_add(1, Ordering::SeqCst); Self(inner) } } impl Clone for TestData { fn clone(&self) -> Self { let inner = self.0.clone(); let _ = inner.fetch_add(1, Ordering::SeqCst); Self(inner) } } impl Drop for TestData { fn drop(&mut self) { self.0.fetch_sub(1, Ordering::SeqCst); } } let num = Arc::new(AtomicUsize::new(0)); let data = TestData::new(num.clone()); assert_eq!(num.load(Ordering::SeqCst), 1); let srv = actix_test::start(move || { let data = data.clone(); App::new() .data(data) .service(web::resource("/").to(|_data: web::Data| async { "ok" })) }); assert!(srv.get("/").send().await.unwrap().status().is_success()); srv.stop().await; assert_eq!(num.load(Ordering::SeqCst), 0); } #[actix_rt::test] async fn test_accept_encoding_no_match() { let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(HttpResponse::Ok))) }); let mut res = srv .get("/") .insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE); assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); let bytes = res.body().await.unwrap(); // body should contain the supported encodings assert!(!bytes.is_empty()); srv.stop().await; } actix-web-4.9.0/tests/test_weird_poll.rs000064400000000000000000000015611046102023000164330ustar 00000000000000//! Regression test for https://github.com/actix/actix-web/issues/1321 // use actix_http::body::{BodyStream, MessageBody}; // use bytes::Bytes; // use futures_channel::oneshot; // use futures_util::{ // stream::once, // task::{noop_waker, Context}, // }; // #[test] // fn weird_poll() { // let (sender, receiver) = oneshot::channel(); // let mut body_stream = Ok(BodyStream::new(once(async { // let x = Box::new(0); // let y = &x; // receiver.await.unwrap(); // let _z = **y; // Ok::<_, ()>(Bytes::new()) // }))); // let waker = noop_waker(); // let mut cx = Context::from_waker(&waker); // let _ = body_stream.as_mut().unwrap().poll_next(&mut cx); // sender.send(()).unwrap(); // let _ = std::mem::replace(&mut body_stream, Err([0; 32])) // .unwrap() // .poll_next(&mut cx); // } actix-web-4.9.0/tests/utils.rs000064400000000000000000000047361046102023000144030ustar 00000000000000// compiling some tests will trigger unused function warnings even though other tests use them #![allow(dead_code)] use std::io::{Read as _, Write as _}; pub mod gzip { use flate2::{read::GzDecoder, write::GzEncoder, Compression}; use super::*; pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); encoder.finish().unwrap() } pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { let mut decoder = GzDecoder::new(bytes.as_ref()); let mut buf = Vec::new(); decoder.read_to_end(&mut buf).unwrap(); buf } } pub mod deflate { use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; use super::*; pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); encoder.finish().unwrap() } pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { let mut decoder = ZlibDecoder::new(bytes.as_ref()); let mut buf = Vec::new(); decoder.read_to_end(&mut buf).unwrap(); buf } } pub mod brotli { use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder}; use super::*; pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = BrotliEncoder::new( Vec::new(), 8 * 1024, // 32 KiB buffer 3, // BROTLI_PARAM_QUALITY 22, // BROTLI_PARAM_LGWIN ); encoder.write_all(bytes.as_ref()).unwrap(); encoder.flush().unwrap(); encoder.into_inner() } pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096); let mut buf = Vec::new(); decoder.read_to_end(&mut buf).unwrap(); buf } } pub mod zstd { use ::zstd::stream::{read::Decoder, write::Encoder}; use super::*; pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = Encoder::new(Vec::new(), 3).unwrap(); encoder.write_all(bytes.as_ref()).unwrap(); encoder.finish().unwrap() } pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { let mut decoder = Decoder::new(bytes.as_ref()).unwrap(); let mut buf = Vec::new(); decoder.read_to_end(&mut buf).unwrap(); buf } } actix-web-4.9.0/tests/weird_poll.rs000064400000000000000000000015611046102023000153740ustar 00000000000000//! Regression test for https://github.com/actix/actix-web/issues/1321 // use actix_http::body::{BodyStream, MessageBody}; // use bytes::Bytes; // use futures_channel::oneshot; // use futures_util::{ // stream::once, // task::{noop_waker, Context}, // }; // #[test] // fn weird_poll() { // let (sender, receiver) = oneshot::channel(); // let mut body_stream = Ok(BodyStream::new(once(async { // let x = Box::new(0); // let y = &x; // receiver.await.unwrap(); // let _z = **y; // Ok::<_, ()>(Bytes::new()) // }))); // let waker = noop_waker(); // let mut cx = Context::from_waker(&waker); // let _ = body_stream.as_mut().unwrap().poll_next(&mut cx); // sender.send(()).unwrap(); // let _ = std::mem::replace(&mut body_stream, Err([0; 32])) // .unwrap() // .poll_next(&mut cx); // }