Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ServiceConfig::default_service #2743

Merged
merged 3 commits into from Apr 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions actix-web/CHANGES.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion actix-web/src/app.rs
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)));
Expand Down
100 changes: 78 additions & 22 deletions actix-web/src/config.rs
@@ -1,41 +1,40 @@
use std::net::SocketAddr;
use std::rc::Rc;

use actix_http::Extensions;
use actix_router::ResourceDef;
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};

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 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<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>>,
)>,
}

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,
Expand All @@ -56,7 +55,7 @@ impl AppService {
AppConfig,
Vec<(
ResourceDef,
HttpNewService,
BoxedHttpServiceFactory,
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
Expand All @@ -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()
}

Expand Down Expand Up @@ -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 {
Expand All @@ -195,6 +195,7 @@ impl ServiceConfig {
services: Vec::new(),
external: Vec::new(),
app_data: Extensions::new(),
default: None,
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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| {
Expand Down
4 changes: 4 additions & 0 deletions actix-web/src/scope.rs
Expand Up @@ -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
}

Expand Down