From b1c85ba85be91b5ea34f31264853b411fadce1ef Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 23 Apr 2022 22:11:45 +0100
Subject: [PATCH] Add `ServiceConfig::default_service` (#2743)

* Add `ServiceConfig::default_service`

based on https://github.com/actix/actix-web/pull/2338

* update changelog
---
 actix-web/CHANGES.md    |  7 ++-
 actix-web/src/app.rs    |  8 +++-
 actix-web/src/config.rs | 96 ++++++++++++++++++++++++++++++++---------
 actix-web/src/scope.rs  |  4 ++
 4 files changed, 92 insertions(+), 23 deletions(-)

diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md
index f8918940..4a16073a 100644
--- a/actix-web/CHANGES.md
+++ b/actix-web/CHANGES.md
@@ -4,13 +4,16 @@
 ### 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]
 
 ### 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 - 2022-02-25
@@ -726,7 +729,7 @@
 ### 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 
+- 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
@@ -828,7 +831,7 @@
 
 ## 3.0.0-beta.4 - 2020-09-09
 ### Added
-- `middleware::NormalizePath` now has configurable behavior for either always having a trailing 
+- `middleware::NormalizePath` now has configurable behavior for either always having a trailing
   slash, or as the new addition, always trimming trailing slashes. [#1639]
 
 ### Changed
diff --git a/actix-web/src/app.rs b/actix-web/src/app.rs
index 18749d34..119980a0 100644
--- a/actix-web/src/app.rs
+++ b/actix-web/src/app.rs
@@ -185,10 +185,17 @@ 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
     }
 
@@ -267,7 +274,6 @@ where
     {
         let svc = svc
             .into_factory()
-            .map(|res| res.map_into_boxed_body())
             .map_init_err(|e| log::error!("Can not construct default service: {:?}", e));
 
         self.default = Some(Rc::new(boxed::factory(svc)));
diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs
index 77fba18e..dab30917 100644
--- a/actix-web/src/config.rs
+++ b/actix-web/src/config.rs
@@ -1,33 +1,32 @@
-use std::net::SocketAddr;
-use std::rc::Rc;
+use std::{net::SocketAddr, rc::Rc};
 
-use actix_http::Extensions;
-use actix_router::ResourceDef;
-use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
+use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
 
-use crate::data::Data;
-use crate::error::Error;
-use crate::guard::Guard;
-use crate::resource::Resource;
-use crate::rmap::ResourceMap;
-use crate::route::Route;
-use crate::service::{
-    AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest,
-    ServiceResponse,
+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<Box<dyn Guard>>;
-type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
 
 /// Application configuration
 pub struct AppService {
     config: AppConfig,
     root: bool,
-    default: Rc<HttpNewService>,
+    default: Rc<BoxedHttpServiceFactory>,
     #[allow(clippy::type_complexity)]
     services: Vec<(
         ResourceDef,
-        HttpNewService,
+        BoxedHttpServiceFactory,
         Option<Guards>,
         Option<Rc<ResourceMap>>,
     )>,
@@ -35,7 +34,7 @@ pub struct AppService {
 
 impl AppService {
     /// Crate server settings instance.
-    pub(crate) fn new(config: AppConfig, default: Rc<HttpNewService>) -> Self {
+    pub(crate) fn new(config: AppConfig, default: Rc<BoxedHttpServiceFactory>) -> Self {
         AppService {
             config,
             default,
@@ -56,7 +55,7 @@ impl AppService {
         AppConfig,
         Vec<(
             ResourceDef,
-            HttpNewService,
+            BoxedHttpServiceFactory,
             Option<Guards>,
             Option<Rc<ResourceMap>>,
         )>,
@@ -81,7 +80,7 @@ impl AppService {
     }
 
     /// Returns default handler factory.
-    pub fn default_service(&self) -> Rc<HttpNewService> {
+    pub fn default_service(&self) -> Rc<BoxedHttpServiceFactory> {
         self.default.clone()
     }
 
@@ -187,6 +186,7 @@ pub struct ServiceConfig {
     pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
     pub(crate) external: Vec<ResourceDef>,
     pub(crate) app_data: Extensions,
+    pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
 }
 
 impl ServiceConfig {
@@ -195,6 +195,7 @@ impl ServiceConfig {
             services: Vec::new(),
             external: Vec::new(),
             app_data: Extensions::new(),
+            default: None,
         }
     }
 
@@ -215,6 +216,29 @@ impl ServiceConfig {
         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<F, U>(&mut self, f: F) -> &mut Self
+    where
+        F: IntoServiceFactory<U, ServiceRequest>,
+        U: ServiceFactory<
+                ServiceRequest,
+                Config = (),
+                Response = ServiceResponse,
+                Error = Error,
+            > + '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.
@@ -322,6 +346,38 @@ mod tests {
         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| {
diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs
index 0fcc83d7..f8c042a5 100644
--- a/actix-web/src/scope.rs
+++ b/actix-web/src/scope.rs
@@ -198,6 +198,10 @@ where
             .get_or_insert_with(Extensions::new)
             .extend(cfg.app_data);
 
+        if let Some(default) = cfg.default {
+            self.default = Some(default);
+        }
+
         self
     }