diff --git a/CHANGES.md b/CHANGES.md index 0077cd518..f0b55801b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,25 +2,31 @@ ## Unreleased - 2021-xx-xx ### Added -* `Compat` middleware enabling generic response body/error type of middlewares - like `Logger` and `Compress` to be used in `middleware::Condition` - and `Resource`, `Scope` services. [#1865] +* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and + `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] ### Changed * Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] * Bumped `rand` to `0.8`. * Update `rust-tls` to `0.19`. [#1813] * Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] +* The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration + guide for implications. [#1875] +* Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] * MSRV is now 1.46.0. -[#1813]: https://github.com/actix/actix-web/pull/1813 -[#1865]: https://github.com/actix/actix-web/pull/1865 - ### Fixed -* added the actual parsing error to `test::read_body_json` [#1812] +* Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] + +### Removed +* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now + exposed directly by 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 +[#1875]: https://github.com/actix/actix-web/pull/1875 ## 3.3.2 - 2020-12-01 diff --git a/MIGRATION.md b/MIGRATION.md index 5c4650194..e01702868 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,15 @@ ## Unreleased +* The default `NormalizePath` behavior now strips trailing slashes by default. This was + previously documented to be the case in v3 but the behavior now matches. The effect is that + routes defined with trailing slashes will become inaccessible when + using `NormalizePath::default()`. + + Before: `#[get("/test/")` + After: `#[get("/test")` + + Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. + ## 3.0.0 diff --git a/examples/basic.rs b/examples/basic.rs index 8b2bf2319..e8ad5fcdb 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -18,8 +18,7 @@ async fn no_params() -> &'static str { #[actix_web::main] async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); - env_logger::init(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); HttpServer::new(|| { App::new() diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 66346b6d6..eabd1190d 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -1,41 +1,45 @@ -//! `Middleware` for enabling any middleware to be used in `Resource`, `Scope` and `Condition`. -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +//! For middleware documentation, see [`Compat`]. -use actix_http::body::Body; -use actix_http::body::{MessageBody, ResponseBody}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_service::{Service, Transform}; -use futures_core::future::LocalBoxFuture; -use futures_core::ready; +use futures_core::{future::LocalBoxFuture, ready}; -use crate::error::Error; -use crate::service::ServiceResponse; +use crate::{error::Error, service::ServiceResponse}; -/// `Middleware` for enabling any middleware to be used in `Resource`, `Scope` and `Condition`. -/// -/// -/// ## Usage +/// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), +/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). /// +/// # Usage /// ```rust /// use actix_web::middleware::{Logger, Compat}; /// use actix_web::{App, web}; /// /// let logger = Logger::default(); /// -/// // this would not compile -/// // let app = App::new().service(web::scope("scoped").wrap(logger)); +/// // this would not compile because of incompatible body types +/// // let app = App::new() +/// // .service(web::scope("scoped").wrap(logger)); /// -/// // by using scoped middleware we can use logger in scope. -/// let app = App::new().service(web::scope("scoped").wrap(Compat::new(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 { - pub fn new(transform: T) -> Self { - Self { transform } + /// Wrap a middleware to give it broader compatibility. + pub fn new(middleware: T) -> Self { + Self { + transform: middleware, + } } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index b6ad2c4b5..376719ab2 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,45 +1,47 @@ -//! `Middleware` for compressing response body. -use std::cmp; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::str::FromStr; -use std::task::{Context, Poll}; +//! For middleware documentation, see [`Compress`]. -use actix_http::body::MessageBody; -use actix_http::encoding::Encoder; -use actix_http::http::header::{ContentEncoding, ACCEPT_ENCODING}; -use actix_http::Error; +use std::{ + cmp, + future::Future, + marker::PhantomData, + pin::Pin, + str::FromStr, + task::{Context, Poll}, +}; + +use actix_http::{ + body::MessageBody, + encoding::Encoder, + http::header::{ContentEncoding, ACCEPT_ENCODING}, + Error, +}; use actix_service::{Service, Transform}; use futures_util::future::{ok, Ready}; use pin_project::pin_project; -use crate::dev::BodyEncoding; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{ + dev::BodyEncoding, + service::{ServiceRequest, ServiceResponse}, +}; -#[derive(Debug, Clone)] -/// `Middleware` for compressing response body. +/// Middleware for compressing response payloads. /// -/// Use `BodyEncoding` trait for overriding response compression. -/// To disable compression set encoding to `ContentEncoding::Identity` value. +/// Use `BodyEncoding` trait for overriding response compression. To disable compression set +/// encoding to `ContentEncoding::Identity`. /// +/// # Usage /// ```rust /// use actix_web::{web, middleware, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new() -/// .wrap(middleware::Compress::default()) -/// .service( -/// web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// let app = App::new() +/// .wrap(middleware::Compress::default()) +/// .default_service(web::to(|| HttpResponse::NotFound())); /// ``` +#[derive(Debug, Clone)] pub struct Compress(ContentEncoding); impl Compress { - /// Create new `Compress` middleware with default encoding. + /// Create new `Compress` middleware with the specified encoding. pub fn new(encoding: ContentEncoding) -> Self { Compress(encoding) } @@ -84,9 +86,7 @@ where type Error = Error; type Future = CompressResponse; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { @@ -109,7 +109,6 @@ where } } -#[doc(hidden)] #[pin_project] pub struct CompressResponse where diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index 04173e053..d61e7d576 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -1,35 +1,36 @@ -//! `Middleware` for conditionally enables another middleware. +//! For middleware documentation, see [`Condition`]. + use std::task::{Context, Poll}; use actix_service::{Service, Transform}; -use futures_util::future::{ok, Either, FutureExt, LocalBoxFuture}; +use futures_util::future::{Either, FutureExt, LocalBoxFuture}; -/// `Middleware` for conditionally enables another middleware. -/// The controlled middleware must not change the `Service` interfaces. +/// Middleware for conditionally enabling other middleware. /// -/// This means you cannot control such middlewares like `Logger` or `Compress` directly. -/// *. See `Compat` middleware for alternative. -/// -/// ## Usage +/// The controlled middleware must not change the `Service` interfaces. This means you cannot +/// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat) +/// middleware for a workaround. /// +/// # Usage /// ```rust /// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::App; /// -/// # fn main() { -/// let enable_normalize = std::env::var("NORMALIZE_PATH") == Ok("true".into()); +/// let enable_normalize = std::env::var("NORMALIZE_PATH").is_ok(); /// let app = App::new() /// .wrap(Condition::new(enable_normalize, NormalizePath::default())); -/// # } /// ``` pub struct Condition { - trans: T, + transformer: T, enable: bool, } impl Condition { - pub fn new(enable: bool, trans: T) -> Self { - Self { trans, enable } + pub fn new(enable: bool, transformer: T) -> Self { + Self { + transformer, + enable, + } } } @@ -49,16 +50,15 @@ where fn new_transform(&self, service: S) -> Self::Future { if self.enable { - let f = self.trans.new_transform(service).map(|res| { - res.map( - ConditionMiddleware::Enable as fn(T::Transform) -> Self::Transform, - ) - }); - Either::Left(f) + let fut = self.transformer.new_transform(service); + async move { + let wrapped_svc = fut.await?; + Ok(ConditionMiddleware::Enable(wrapped_svc)) + } + .boxed_local() } else { - Either::Right(ok(ConditionMiddleware::Disable(service))) + async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local() } - .boxed_local() } } @@ -77,18 +77,16 @@ where type Future = Either; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - use ConditionMiddleware::*; match self { - Enable(service) => service.poll_ready(cx), - Disable(service) => service.poll_ready(cx), + ConditionMiddleware::Enable(service) => service.poll_ready(cx), + ConditionMiddleware::Disable(service) => service.poll_ready(cx), } } fn call(&mut self, req: Req) -> Self::Future { - use ConditionMiddleware::*; match self { - Enable(service) => Either::Left(service.call(req)), - Disable(service) => Either::Right(service.call(req)), + ConditionMiddleware::Enable(service) => Either::Left(service.call(req)), + ConditionMiddleware::Disable(service) => Either::Right(service.call(req)), } } } @@ -96,14 +94,17 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; + use futures_util::future::ok; use super::*; - use crate::dev::{ServiceRequest, ServiceResponse}; - use crate::error::Result; - use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; - use crate::middleware::errhandlers::*; - use crate::test::{self, TestRequest}; - use crate::HttpResponse; + use crate::{ + dev::{ServiceRequest, ServiceResponse}, + error::Result, + http::{header::CONTENT_TYPE, HeaderValue, StatusCode}, + middleware::err_handlers::*, + test::{self, TestRequest}, + HttpResponse, + }; #[allow(clippy::unnecessary_wraps)] fn render_500(mut res: ServiceResponse) -> Result> { diff --git a/src/middleware/defaultheaders.rs b/src/middleware/default_headers.rs similarity index 79% rename from src/middleware/defaultheaders.rs rename to src/middleware/default_headers.rs index d648ad70f..6f027124f 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/default_headers.rs @@ -1,24 +1,34 @@ -//! Middleware for setting default response headers -use std::convert::TryFrom; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; +//! For middleware documentation, see [`DefaultHeaders`]. -use actix_service::{Service, Transform}; -use futures_util::future::{ready, Ready}; -use futures_util::ready; +use std::{ + convert::TryFrom, + future::Future, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; -use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use crate::http::{Error as HttpError, HeaderMap}; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::Error; +use futures_util::{ + future::{ready, Ready}, + ready, +}; -/// `Middleware` for setting default response headers. +use crate::{ + dev::{Service, Transform}, + http::{ + header::{HeaderName, HeaderValue, CONTENT_TYPE}, + Error as HttpError, HeaderMap, + }, + service::{ServiceRequest, ServiceResponse}, + Error, +}; + +/// Middleware for setting default response headers. /// -/// This middleware does not set header if response headers already contains it. +/// Headers with the same key that are already set in a response will *not* be overwritten. /// +/// # Usage /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; /// @@ -38,7 +48,6 @@ pub struct DefaultHeaders { } struct Inner { - ct: bool, headers: HeaderMap, } @@ -46,7 +55,6 @@ impl Default for DefaultHeaders { fn default() -> Self { DefaultHeaders { inner: Rc::new(Inner { - ct: false, headers: HeaderMap::new(), }), } @@ -54,12 +62,12 @@ impl Default for DefaultHeaders { } impl DefaultHeaders { - /// Construct `DefaultHeaders` middleware. + /// Constructs an empty `DefaultHeaders` middleware. pub fn new() -> DefaultHeaders { DefaultHeaders::default() } - /// Set a header. + /// Adds a header to the default set. #[inline] pub fn header(mut self, key: K, value: V) -> Self where @@ -84,11 +92,18 @@ impl DefaultHeaders { self } - /// Set *CONTENT-TYPE* header if response does not contain this header. - pub fn content_type(mut self) -> Self { + /// Adds a default *Content-Type* header if response does not contain one. + /// + /// Default is `application/octet-stream`. + pub fn add_content_type(mut self) -> Self { Rc::get_mut(&mut self.inner) - .expect("Multiple copies exist") - .ct = true; + .expect("Multiple `Inner` copies exist.") + .headers + .insert( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + self } } @@ -126,9 +141,7 @@ where type Error = Error; type Future = DefaultHeaderFuture; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); fn call(&mut self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); @@ -160,19 +173,14 @@ where 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()); } } - // default content-type - if this.inner.ct && !res.headers().contains_key(&CONTENT_TYPE) { - res.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); - } + Poll::Ready(Ok(res)) } } @@ -183,10 +191,12 @@ mod tests { use futures_util::future::ok; use super::*; - use crate::dev::ServiceRequest; - use crate::http::header::CONTENT_TYPE; - use crate::test::{ok_service, TestRequest}; - use crate::HttpResponse; + use crate::{ + dev::ServiceRequest, + http::header::CONTENT_TYPE, + test::{ok_service, TestRequest}, + HttpResponse, + }; #[actix_rt::test] async fn test_default_headers() { @@ -219,7 +229,7 @@ mod tests { let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); let mut mw = DefaultHeaders::new() - .content_type() + .add_content_type() .new_transform(srv.into_service()) .await .unwrap(); diff --git a/src/middleware/errhandlers.rs b/src/middleware/err_handlers.rs similarity index 85% rename from src/middleware/errhandlers.rs rename to src/middleware/err_handlers.rs index 8503b3e1e..dfd9a7dc5 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/err_handlers.rs @@ -1,35 +1,36 @@ -//! Custom handlers service for responses. +//! For middleware documentation, see [`ErrorHandlers`]. + use std::rc::Rc; -use std::task::{Context, Poll}; use actix_service::{Service, Transform}; use ahash::AHashMap; use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use crate::dev::{ServiceRequest, ServiceResponse}; -use crate::error::{Error, Result}; -use crate::http::StatusCode; +use crate::{ + dev::{ServiceRequest, ServiceResponse}, + error::{Error, Result}, + http::StatusCode, +}; -/// Error handler response +/// Return type for [`ErrorHandlers`] custom handlers. pub enum ErrorHandlerResponse { - /// New http response got generated + /// Immediate HTTP response. Response(ServiceResponse), - /// Result is a future that resolves to a new http response + + /// A future that resolves to an HTTP response. Future(LocalBoxFuture<'static, Result, Error>>), } type ErrorHandler = dyn Fn(ServiceResponse) -> Result>; -/// `Middleware` for allowing custom handlers for responses. +/// Middleware for registering custom status code based error handlers. /// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example +/// 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. /// +/// # Usage /// ```rust -/// use actix_web::middleware::errhandlers::{ErrorHandlers, ErrorHandlerResponse}; +/// use actix_web::middleware::{ErrorHandlers, ErrorHandlerResponse}; /// use actix_web::{web, http, dev, App, HttpRequest, HttpResponse, Result}; /// /// fn render_500(mut res: dev::ServiceResponse) -> Result> { @@ -39,7 +40,6 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result = dyn Fn(ServiceResponse) -> Result { handlers: Rc>>>, @@ -64,12 +63,12 @@ impl Default for ErrorHandlers { } impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance + /// Construct new `ErrorHandlers` instance. pub fn new() -> Self { ErrorHandlers::default() } - /// Register error handler for specified status code + /// Register error handler for specified status code. pub fn handler(mut self, status: StatusCode, handler: F) -> Self where F: Fn(ServiceResponse) -> Result> + 'static, @@ -117,9 +116,7 @@ where type Error = Error; type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); fn call(&mut self, req: ServiceRequest) -> Self::Future { let handlers = self.handlers.clone(); diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index c952aeac9..cdbd5e485 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -1,13 +1,16 @@ -//! Request logging middleware -use std::collections::HashSet; -use std::convert::TryFrom; -use std::env; -use std::fmt::{self, Display, Formatter}; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; +//! For middleware documentation, see [`Logger`]. + +use std::{ + collections::HashSet, + convert::TryFrom, + env, + fmt::{self, Display as _}, + future::Future, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_service::{Service, Transform}; use bytes::Bytes; @@ -16,78 +19,69 @@ use log::debug; use regex::{Regex, RegexSet}; use time::OffsetDateTime; -use crate::dev::{BodySize, MessageBody, ResponseBody}; -use crate::error::{Error, Result}; -use crate::http::{HeaderName, StatusCode}; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::HttpResponse; +use crate::{ + dev::{BodySize, MessageBody, ResponseBody}, + error::{Error, Result}, + http::{HeaderName, StatusCode}, + service::{ServiceRequest, ServiceResponse}, + HttpResponse, +}; -/// `Middleware` for logging request and response info to the terminal. +/// Middleware for logging request and response summaries to the terminal. /// -/// `Logger` middleware uses standard log crate to log information. You should -/// enable logger for `actix_web` package to see access log. -/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) +/// 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. /// -/// ## Usage -/// -/// Create `Logger` middleware with the specified `format`. -/// Default `Logger` could be created with `default` method, it uses the -/// default format: +/// # 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 /// ``` /// +/// # Usage /// ```rust /// use actix_web::{middleware::Logger, App}; /// -/// std::env::set_var("RUST_LOG", "actix_web=info"); -/// env_logger::init(); +/// // 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::default()) /// .wrap(Logger::new("%a %{User-Agent}i")); /// ``` /// -/// ## Format -/// -/// `%%` The percent sign -/// -/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) -/// -/// `%t` Time when the request was started to process (in rfc3339 format) -/// -/// `%r` First line of request -/// -/// `%s` Response status code -/// -/// `%b` Size of response in bytes, including HTTP headers -/// -/// `%T` Time taken to serve the request, in seconds with floating fraction in -/// .06f format -/// -/// `%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` os.environ['FOO'] -/// -/// `%{FOO}xi` [custom request replacement](Logger::custom_request_replace) labelled "FOO" +/// # 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" /// /// # Security -/// **\*** It is calculated using -/// [`ConnectionInfo::realip_remote_addr()`](crate::dev::ConnectionInfo::realip_remote_addr()) +/// **\*** "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, since it is trivial -/// for the remote client to simulate being another client. +/// 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, @@ -113,7 +107,7 @@ impl Logger { self } - /// Ignore and do not log access info for paths that match regex + /// 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(); let mut patterns = inner.exclude_regex.patterns().to_vec(); @@ -209,7 +203,7 @@ where } } -/// Logger middleware +/// Logger middleware service. pub struct LoggerMiddleware { inner: Rc, service: S, @@ -224,9 +218,7 @@ where type Error = Error; type Future = LoggerResponse; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); fn call(&mut self, req: ServiceRequest) -> Self::Future { if self.inner.exclude.contains(req.path()) @@ -255,7 +247,6 @@ where } } -#[doc(hidden)] #[pin_project::pin_project] pub struct LoggerResponse where @@ -325,7 +316,7 @@ pub struct StreamLog { impl PinnedDrop for StreamLog { fn drop(self: Pin<&mut Self>) { if let Some(ref format) = self.format { - let render = |fmt: &mut Formatter<'_>| { + let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, self.size, self.time)?; } @@ -356,9 +347,8 @@ impl MessageBody for StreamLog { } } -/// A formatting style for the `Logger`, consisting of multiple -/// `FormatText`s concatenated into one line. -#[derive(Clone)] +/// A formatting style for the `Logger` consisting of multiple concatenated `FormatText` items. +#[derive(Debug, Clone)] struct Format(Vec); impl Default for Format { @@ -430,13 +420,12 @@ impl Format { } } -/// A string of text to be logged. This is either one of the data -/// fields supported by the `Logger`, or a custom `String`. -#[doc(hidden)] +/// 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)] -// TODO: remove pub on next breaking change -pub enum FormatText { +enum FormatText { Str(String), Percent, RequestLine, @@ -454,10 +443,8 @@ pub enum FormatText { CustomRequest(String, Option), } -// TODO: remove pub on next breaking change -#[doc(hidden)] #[derive(Clone)] -pub struct CustomRequestFn { +struct CustomRequestFn { inner_fn: Rc String>, } @@ -476,11 +463,11 @@ impl fmt::Debug for CustomRequestFn { impl FormatText { fn render( &self, - fmt: &mut Formatter<'_>, + fmt: &mut fmt::Formatter<'_>, size: usize, entry_time: OffsetDateTime, ) -> Result<(), fmt::Error> { - match *self { + match self { FormatText::Str(ref string) => fmt.write_str(string), FormatText::Percent => "%".fmt(fmt), FormatText::ResponseSize => size.fmt(fmt), @@ -506,7 +493,7 @@ impl FormatText { } fn render_response(&mut self, res: &HttpResponse) { - match *self { + match self { FormatText::ResponseStatus => { *self = FormatText::Str(format!("{}", res.status().as_u16())) } @@ -527,7 +514,7 @@ impl FormatText { } fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) { - match &*self { + match self { FormatText::RequestLine => { *self = if req.query_string().is_empty() { FormatText::Str(format!( @@ -594,11 +581,11 @@ impl FormatText { /// Converter to get a String from something that writes to a Formatter. pub(crate) struct FormatDisplay<'a>( - &'a dyn Fn(&mut Formatter<'_>) -> Result<(), fmt::Error>, + &'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>, ); impl<'a> fmt::Display for FormatDisplay<'a> { - fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { (self.0)(fmt) } } @@ -675,7 +662,7 @@ mod tests { unit.render_response(&resp); } - let render = |fmt: &mut Formatter<'_>| { + let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, now)?; } @@ -708,7 +695,7 @@ mod tests { } let entry_time = OffsetDateTime::now_utc(); - let render = |fmt: &mut Formatter<'_>| { + let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; } @@ -736,7 +723,7 @@ mod tests { unit.render_response(&resp); } - let render = |fmt: &mut Formatter<'_>| { + let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, now)?; } @@ -769,7 +756,7 @@ mod tests { } let entry_time = OffsetDateTime::now_utc(); - let render = |fmt: &mut Formatter<'_>| { + let render = |fmt: &mut fmt::Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; } @@ -800,7 +787,7 @@ mod tests { unit.render_request(now, &req); - let render = |fmt: &mut Formatter<'_>| unit.render(fmt, 1024, now); + let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now); let log_output = FormatDisplay(&render).to_string(); assert_eq!(log_output, "custom_log"); diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index af44fd8c7..e24782f07 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,19 +1,20 @@ -//! Middlewares +//! Commonly used middleware. + +mod compat; +mod condition; +mod default_headers; +mod err_handlers; +mod logger; +mod normalize; + +pub use self::compat::Compat; +pub use self::condition::Condition; +pub use self::default_headers::DefaultHeaders; +pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; +pub use self::logger::Logger; +pub use self::normalize::{NormalizePath, TrailingSlash}; #[cfg(feature = "compress")] mod compress; #[cfg(feature = "compress")] pub use self::compress::Compress; - -mod compat; -mod condition; -mod defaultheaders; -pub mod errhandlers; -mod logger; -pub mod normalize; - -pub use self::compat::Compat; -pub use self::condition::Condition; -pub use self::defaultheaders::DefaultHeaders; -pub use self::logger::Logger; -pub use self::normalize::NormalizePath; diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 4109364bf..89cab9073 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -1,60 +1,63 @@ //! For middleware documentation, see [`NormalizePath`]. -use std::task::{Context, Poll}; - use actix_http::http::{PathAndQuery, Uri}; use actix_service::{Service, Transform}; use bytes::Bytes; use futures_util::future::{ready, Ready}; use regex::Regex; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::Error; +use crate::{ + service::{ServiceRequest, ServiceResponse}, + Error, +}; -/// To be used when constructing `NormalizePath` to define it's behavior. +/// Determines the behavior of the [`NormalizePath`] middleware. +/// +/// The default is `TrailingSlash::Trim`. #[non_exhaustive] -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub enum TrailingSlash { - /// Always add a trailing slash to the end of the path. - /// This will require all routes to end in a trailing slash for them to be accessible. - Always, + /// Trim trailing slashes from the end of the path. + /// + /// Using this will require all routes to omit trailing slashes for them to be accessible. + Trim, /// Only merge any present multiple trailing slashes. /// - /// Note: This option provides the best compatibility with the v2 version of this middleware. + /// This option provides the best compatibility with behavior in actix-web v2.0. MergeOnly, - /// Trim trailing slashes from the end of the path. - Trim, + /// 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, } impl Default for TrailingSlash { fn default() -> Self { - TrailingSlash::Always + TrailingSlash::Trim } } -#[derive(Default, Clone, Copy)] -/// Middleware to normalize a request's path so that routes can be matched less strictly. +/// Middleware for normalizing a request's path so that routes can be matched more flexibly. /// /// # Normalization Steps -/// - Merges multiple consecutive slashes into one. (For example, `/path//one` always -/// becomes `/path/one`.) +/// - 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 -/// ([`TrailingSlash::Trim`]), the effect is that route definitions should be defined without -/// trailing slashes or else they will be inaccessible. +/// 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. /// -/// # Example +/// # Usage /// ```rust /// use actix_web::{web, middleware, App}; /// -/// # #[actix_rt::test] -/// # async fn normalize() { +/// # actix_web::rt::System::new("doctest").block_on(async { /// let app = App::new() /// .wrap(middleware::NormalizePath::default()) /// .route("/test", web::get().to(|| async { "test" })) @@ -80,8 +83,9 @@ impl Default for TrailingSlash { /// let req = TestRequest::with_uri("/unmatchable/").to_request(); /// let res = call_service(&mut app, req).await; /// assert_eq!(res.status(), StatusCode::NOT_FOUND); -/// # } +/// # }) /// ``` +#[derive(Debug, Clone, Copy, Default)] pub struct NormalizePath(TrailingSlash); impl NormalizePath { @@ -111,7 +115,6 @@ where } } -#[doc(hidden)] pub struct NormalizePathNormalization { service: S, merge_slash: Regex, @@ -127,9 +130,7 @@ where type Error = Error; type Future = S::Future; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); @@ -198,7 +199,7 @@ mod tests { App::new() .wrap(NormalizePath::default()) .service(web::resource("/").to(HttpResponse::Ok)) - .service(web::resource("/v1/something/").to(HttpResponse::Ok)), + .service(web::resource("/v1/something").to(HttpResponse::Ok)), ) .await; @@ -306,7 +307,7 @@ mod tests { #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { - assert_eq!("/v1/something/", req.path()); + assert_eq!("/v1/something", req.path()); ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; @@ -334,7 +335,7 @@ mod tests { #[actix_rt::test] async fn should_normalize_nothing() { - const URI: &str = "/v1/something/"; + const URI: &str = "/v1/something"; let srv = |req: ServiceRequest| { assert_eq!(URI, req.path()); @@ -353,10 +354,8 @@ mod tests { #[actix_rt::test] async fn should_normalize_no_trail() { - const URI: &str = "/v1/something"; - let srv = |req: ServiceRequest| { - assert_eq!(URI.to_string() + "/", req.path()); + assert_eq!("/v1/something", req.path()); ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; @@ -365,7 +364,7 @@ mod tests { .await .unwrap(); - let req = TestRequest::with_uri(URI).to_srv_request(); + let req = TestRequest::with_uri("/v1/something/").to_srv_request(); let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success()); } diff --git a/tests/test_server.rs b/tests/test_server.rs index 2f8ce625e..43ee1230d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,7 +1,9 @@ -use std::future::Future; -use std::io::{Read, Write}; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + future::Future, + io::{Read, Write}, + pin::Pin, + task::{Context, Poll}, +}; use actix_http::http::header::{ ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, @@ -9,15 +11,16 @@ use actix_http::http::header::{ }; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; -use flate2::read::GzDecoder; -use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; -use flate2::Compression; +use flate2::{ + read::GzDecoder, + write::{GzEncoder, ZlibDecoder, ZlibEncoder}, + Compression, +}; use futures_util::ready; use rand::{distributions::Alphanumeric, Rng}; use actix_web::dev::BodyEncoding; -use actix_web::middleware::normalize::TrailingSlash; -use actix_web::middleware::{Compress, NormalizePath}; +use actix_web::middleware::{Compress, NormalizePath, TrailingSlash}; use actix_web::{dev, test, web, App, Error, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \