//! 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, } #[cfg(test)] impl Compat { pub(crate) fn noop() -> Self { Self { transform: super::Noop, } } } 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, 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::noop() .new_transform(srv.into_service()) .await .unwrap(); let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.status(), StatusCode::OK); } }