1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-01 16:55:08 +02:00

major cleanup of middleware module (#1875)

* major cleanup of middleware module

* update changelog
This commit is contained in:
Rob Ede
2021-01-05 09:51:58 +00:00
committed by GitHub
parent 4f5971d79e
commit 68117543ea
12 changed files with 319 additions and 303 deletions

View File

@ -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<T> {
transform: T,
}
impl<T> Compat<T> {
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,
}
}
}

View File

@ -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<S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<S, B>
where

View File

@ -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<T> {
trans: T,
transformer: T,
enable: bool,
}
impl<T> Condition<T> {
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<E::Future, D::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {

View File

@ -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<K, V>(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<S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<Self::Output> {
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();

View File

@ -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<B> {
/// New http response got generated
/// Immediate HTTP response.
Response(ServiceResponse<B>),
/// Result is a future that resolves to a new http response
/// A future that resolves to an HTTP response.
Future(LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>),
}
type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>;
/// `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<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
@ -39,7 +40,6 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
/// Ok(ErrorHandlerResponse::Response(res))
/// }
///
/// # fn main() {
/// let app = App::new()
/// .wrap(
/// ErrorHandlers::new()
@ -49,7 +49,6 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
/// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())
/// ));
/// # }
/// ```
pub struct ErrorHandlers<B> {
handlers: Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>,
@ -64,12 +63,12 @@ impl<B> Default for ErrorHandlers<B> {
}
impl<B> ErrorHandlers<B> {
/// 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<F>(mut self, status: StatusCode, handler: F) -> Self
where
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
@ -117,9 +116,7 @@ where
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
actix_service::forward_ready!(service);
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let handlers = self.handlers.clone();

View File

@ -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<Inner>);
#[derive(Debug, Clone)]
struct Inner {
format: Format,
exclude: HashSet<String>,
@ -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<T: Into<String>>(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<S> {
inner: Rc<Inner>,
service: S,
@ -224,9 +218,7 @@ where
type Error = Error;
type Future = LoggerResponse<S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<S, B>
where
@ -325,7 +316,7 @@ pub struct StreamLog<B> {
impl<B> PinnedDrop for StreamLog<B> {
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<B: MessageBody> MessageBody for StreamLog<B> {
}
}
/// 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<FormatText>);
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<CustomRequestFn>),
}
// TODO: remove pub on next breaking change
#[doc(hidden)]
#[derive(Clone)]
pub struct CustomRequestFn {
struct CustomRequestFn {
inner_fn: Rc<dyn Fn(&ServiceRequest) -> 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<B>(&mut self, res: &HttpResponse<B>) {
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");

View File

@ -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;

View File

@ -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<S> {
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<Result<(), Self::Error>> {
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());
}