Skip to main content

ntex/web/
scope.rs

1use std::{cell::RefCell, marker::PhantomData, rc::Rc};
2
3use crate::error::{Failure, IntoFailure};
4use crate::http::Response;
5use crate::router::{IntoPattern, ResourceDef, Router};
6use crate::service::{Identity, ServiceChainFactory, boxed};
7use crate::{IntoServiceFactory, Middleware, Service, ServiceFactory, factory};
8
9use super::app_service::WebServiceRouter;
10use super::dev::{WebServiceConfig, WebServiceFactory};
11use super::error::{WebError, WebResponseError};
12use super::guard::Guard;
13use super::rmap::ResourceMap;
14use super::service::{AppServiceFactory, ServiceFactoryWrapper};
15use super::stack::{Filter, WebStack};
16use super::{HttpService, Resource, Route, ServiceConfig, State, WebRequest, WebResponse};
17
18type Guards = Vec<Box<dyn Guard>>;
19
20/// Groups services under a shared path prefix.
21///
22/// A scope is useful for keeping a related part of an application together,
23/// such as an API or administration area. It can contain resources, routes,
24/// nested scopes, middleware, filters, guards, and its own fallback service.
25///
26/// The prefix matches complete path segments. For example, `scope("/api")`
27/// can contain a service for `/api/users`, but the scope itself does not match
28/// the bare `/api` path. Register an empty nested resource if that path also
29/// needs a handler.
30///
31/// Scope prefixes can contain dynamic segments. Their values remain available
32/// to nested handlers through [`HttpRequest::match_info()`] and the [`Path`]
33/// extractor. For example, `scope("/users/{user_id}")` makes `user_id`
34/// available to every handler nested inside that scope.
35///
36/// Once the scope's prefix and guards match, its middleware and filters run,
37/// followed by its nested router. If no nested service matches, the scope uses
38/// its own fallback, which returns `404 Not Found` by default. It does not fall
39/// back to the surrounding application.
40///
41/// ```rust
42/// use ntex::web::{self, App};
43///
44/// async fn issues(
45///     path: web::types::Path<(String,)>,
46/// ) -> String {
47///     format!("Issues for {}", path.0)
48/// }
49///
50/// App::default().service(
51///     web::scope("/projects/{project_id}")
52///         .route("/issues", web::get().to(issues))
53///         .route("/settings", web::get().to(async || "settings")),
54/// );
55/// ```
56///
57/// [`HttpRequest::match_info()`]: super::HttpRequest::match_info
58/// [`Path`]: super::types::Path
59#[derive(derive_more::Debug)]
60#[debug("Scope({rdef:?})")]
61pub struct Scope<St: State, In, Out = In, M = Identity, F = Filter<St, In>> {
62    middleware: M,
63    filter: ServiceChainFactory<F, St, WebRequest<In>>,
64    rdef: Vec<String>,
65    guards: Vec<Box<dyn Guard>>,
66    external: Vec<ResourceDef>,
67    case_insensitive: bool,
68    ph: PhantomData<Out>,
69}
70
71/// A scope with nested services or a configured fallback.
72///
73/// Methods such as [`Scope::service()`], [`Scope::route()`],
74/// [`Scope::configure()`], and [`Scope::default_service()`] turn a [`Scope`]
75/// into `ScopeServices`. You can then add more services or routes and adjust
76/// the scope fallback.
77#[derive(derive_more::Debug)]
78#[debug("ScopeServices({rdef:?})")]
79pub struct ScopeServices<St: State, In, Out, M, F> {
80    middleware: M,
81    rdef: Vec<String>,
82    guards: Vec<Box<dyn Guard>>,
83    filter: ServiceChainFactory<F, St, WebRequest<In>>,
84    services: Vec<Box<dyn AppServiceFactory<St, Out>>>,
85    default: Option<HttpService<St, Out>>,
86    external: Vec<ResourceDef>,
87    case_insensitive: bool,
88}
89
90impl<St: State, In> Scope<St, In, In> {
91    #[allow(clippy::needless_pass_by_value)]
92    /// Create a new scope
93    pub fn new<T: IntoPattern>(path: T) -> Self {
94        Scope {
95            middleware: Identity,
96            filter: factory(Filter::new()),
97            rdef: path.patterns(),
98            guards: Vec::new(),
99            external: Vec::new(),
100            case_insensitive: false,
101            ph: PhantomData,
102        }
103    }
104}
105
106impl<St, In, Out, M, F> Scope<St, In, Out, M, F>
107where
108    St: State,
109    In: 'static,
110    Out: 'static,
111    F: ServiceFactory<
112            St,
113            WebRequest<In>,
114            Res = WebRequest<Out>,
115            Error = WebError<St, St::Error>,
116            InitError = Failure,
117        >,
118{
119    /// Add a match guard to this scope.
120    ///
121    /// The scope is selected only when its path prefix and all registered
122    /// guards match. If a guard rejects the request, the application router can
123    /// try another matching scope or resource; otherwise the application's
124    /// default service is used.
125    ///
126    /// The guard applies to every resource nested in the scope.
127    ///
128    /// ```rust
129    /// use ntex::web::{self, guard, App};
130    ///
131    /// App::default().service(
132    ///     web::scope("/api")
133    ///         .guard(guard::Header("x-api-version", "2"))
134    ///         .route("/users", web::get().to(async || "Version 2 users"))
135    /// );
136    /// ```
137    #[must_use]
138    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
139        self.guards.push(Box::new(guard));
140        self
141    }
142
143    /// Use ascii case-insensitive routing.
144    ///
145    /// Only static segments could be case-insensitive.
146    #[must_use]
147    pub fn case_insensitive_routing(mut self) -> Self {
148        self.case_insensitive = true;
149        self
150    }
151
152    /// Run external configuration as part of the scope building
153    /// process
154    ///
155    /// This function is useful for moving parts of configuration to a
156    /// different module or even library. For example,
157    /// some of the resource's configuration could be moved to different module.
158    ///
159    /// ```rust,ignore
160    /// use ntex::web::{self, middleware, App, HttpResponse};
161    ///
162    /// // this function could be located in different module
163    /// fn config(cfg: &mut web::ServiceConfig) {
164    ///     cfg.service(web::resource("/test")
165    ///         .route(web::get().to(async || { HttpResponse::Ok() }))
166    ///         .route(web::head().to(async || { HttpResponse::MethodNotAllowed() }))
167    ///     );
168    /// }
169    ///
170    /// fn main() {
171    ///     let app = App::default()
172    ///         .middleware(middleware::Logger::default())
173    ///         .service(
174    ///             web::scope("/api")
175    ///                 .configure(config)
176    ///         )
177    ///         .route("/index.html", web::get().to(async || { HttpResponse::Ok() }));
178    /// }
179    /// ```
180    #[must_use]
181    pub fn configure(
182        self,
183        f: impl FnOnce(&mut ServiceConfig<St, Out>),
184    ) -> ScopeServices<St, In, Out, M, F> {
185        let mut cfg = ServiceConfig::new(self.external);
186        f(&mut cfg);
187
188        ScopeServices {
189            rdef: self.rdef,
190            guards: self.guards,
191            filter: self.filter,
192            middleware: self.middleware,
193            default: None,
194            services: cfg.services,
195            external: cfg.external,
196            case_insensitive: self.case_insensitive,
197        }
198    }
199
200    /// Registers a web service for this scope.
201    ///
202    /// The service's path is joined with the scope prefix. A service can be a
203    /// [`Resource`], another [`Scope`], an attribute-macro handler, or a custom
204    /// service built with `web::service()`.
205    ///
206    /// If this scope matches but none of its services do, the scope's default
207    /// service is used.
208    ///
209    /// ```rust
210    /// use ntex::web::{self, App};
211    ///
212    /// App::default().service(
213    ///     web::scope("/api")
214    ///         .service(web::resource("/users").to(async || "users"))
215    ///         .service(
216    ///             web::scope("/admin")
217    ///                 .route("/health", web::get().to(async || "OK")),
218    ///         ),
219    /// );
220    /// ```
221    #[must_use]
222    pub fn service(
223        self,
224        factory: impl WebServiceFactory<St, Out>,
225    ) -> ScopeServices<St, In, Out, M, F> {
226        ScopeServices {
227            rdef: self.rdef,
228            guards: self.guards,
229            filter: self.filter,
230            middleware: self.middleware,
231            default: None,
232            external: self.external,
233            case_insensitive: self.case_insensitive,
234            services: vec![Box::new(ServiceFactoryWrapper::new(factory))],
235        }
236    }
237
238    /// Register a route for a path relative to this scope.
239    ///
240    /// This is shorthand for creating a [`Resource`] with one route and
241    /// registering it with [`Scope::service()`]. The route's method and custom
242    /// guards are promoted to resource guards.
243    ///
244    /// If those guards reject a request, the generated resource does not match
245    /// and the scope router continues searching. If nothing else in the scope
246    /// matches, the scope default service is used. Register an explicit
247    /// [`Resource`] when route mismatches should use a resource-level fallback.
248    ///
249    /// Each call creates a separate resource, so the same relative path can be
250    /// registered more than once with different guards.
251    ///
252    /// ```rust
253    /// use ntex::web::{self, App, HttpResponse};
254    ///
255    /// App::default().service(
256    ///     web::scope("/api")
257    ///         .route("/items", web::get().to(async || "list"))
258    ///         .route(
259    ///             "/items",
260    ///             web::post().to(async || HttpResponse::Created()),
261    ///         )
262    /// );
263    /// ```
264    #[must_use]
265    pub fn route(self, path: &str, mut route: Route<St, Out>) -> ScopeServices<St, In, Out, M, F> {
266        self.service(
267            Resource::new(path)
268                .add_guards(route.take_guards())
269                .route(route),
270        )
271    }
272
273    /// Set the fallback service for unmatched requests within this scope.
274    ///
275    /// The fallback is called after the scope prefix and guards match but no
276    /// nested resource matches. Without a custom fallback, the scope returns
277    /// `404 Not Found`; it does not delegate to the application's fallback.
278    /// Routing failures inside a matched resource are handled by that
279    /// resource's fallback instead.
280    ///
281    /// ```rust
282    /// use ntex::web::{self, App, HttpRequest, HttpResponse};
283    ///
284    /// async fn not_found(req: HttpRequest) -> HttpResponse {
285    ///     HttpResponse::NotFound()
286    ///         .body(format!("No API resource for {}", req.path()))
287    /// }
288    ///
289    /// App::default().service(
290    ///     web::scope("/api")
291    ///         .route("/health", web::get().to(async || "ready"))
292    ///         .default_service(web::to(not_found))
293    /// );
294    /// ```
295    #[must_use]
296    pub fn default_service<Sf>(
297        self,
298        f: impl IntoServiceFactory<Sf, St, WebRequest<Out>>,
299    ) -> ScopeServices<St, In, Out, M, F>
300    where
301        Sf: ServiceFactory<St, WebRequest<Out>, Res = WebResponse> + 'static,
302        Sf::Error: WebResponseError<St, St::Error>,
303        Sf::InitError: IntoFailure,
304    {
305        // create and configure default resource
306        let default = boxed::factory(
307            f.into_factory()
308                .map_err(WebError::from_err)
309                .map_init_err(IntoFailure::fail),
310        );
311
312        ScopeServices {
313            rdef: self.rdef,
314            guards: self.guards,
315            filter: self.filter,
316            middleware: self.middleware,
317            default: Some(default),
318            external: self.external,
319            case_insensitive: self.case_insensitive,
320            services: Vec::new(),
321        }
322    }
323
324    /// Registers a request filter for this scope.
325    ///
326    /// The filter runs after the scope's path and guards match, but before its
327    /// nested router selects a resource. It is not called for requests that do
328    /// not match this scope.
329    ///
330    /// ```rust
331    /// use std::convert::Infallible;
332    /// use ntex::web::{self, App, WebRequest};
333    ///
334    /// async fn user(_state: &(), user_id: usize) -> String {
335    ///     format!("User {user_id}")
336    /// }
337    ///
338    /// App::new().service(
339    ///     web::scope("/api")
340    ///         .filter(async |req: WebRequest<()>| {
341    ///             Ok::<_, Infallible>(req.map_state(|()| 42usize))
342    ///         })
343    ///         .route("/user", web::get().to_with_state(user)),
344    /// );
345    /// ```
346    #[must_use]
347    pub fn filter<U, R>(
348        self,
349        filter: impl IntoServiceFactory<U, St, WebRequest<Out>>,
350    ) -> Scope<
351        St,
352        In,
353        R,
354        M,
355        impl ServiceFactory<
356            St,
357            WebRequest<In>,
358            Res = WebRequest<R>,
359            Error = WebError<St, St::Error>,
360            InitError = Failure,
361        >,
362    >
363    where
364        U: ServiceFactory<St, WebRequest<Out>, Res = WebRequest<R>>,
365        U::Error: WebResponseError<St, St::Error>,
366        U::InitError: IntoFailure,
367    {
368        Scope {
369            filter: self.filter.and_then(
370                filter
371                    .into_factory()
372                    .map_err(WebError::from_err)
373                    .map_init_err(IntoFailure::fail),
374            ),
375            middleware: self.middleware,
376            rdef: self.rdef,
377            guards: self.guards,
378            external: self.external,
379            case_insensitive: self.case_insensitive,
380            ph: PhantomData,
381        }
382    }
383
384    /// Registers a middleware for this scope.
385    ///
386    /// The middleware runs only after the scope's path and guards match. It
387    /// wraps everything inside the scope, including its filter, nested routes,
388    /// and fallback service. This means it can inspect or modify both the
389    /// request and response, even when no nested route matches.
390    ///
391    /// ```rust
392    /// use ntex::web::{self, middleware, App};
393    ///
394    /// App::default().service(
395    ///     web::scope("/api")
396    ///         .middleware(
397    ///             middleware::DefaultHeaders::new()
398    ///                 .header("x-api", "v1"),
399    ///         )
400    ///         .route("/health", web::get().to(async || "OK")),
401    /// );
402    /// ```
403    #[must_use]
404    pub fn middleware<U>(self, mw: U) -> Scope<St, In, Out, WebStack<St, U, M>, F> {
405        Scope {
406            middleware: WebStack::new(mw, self.middleware),
407            filter: self.filter,
408            rdef: self.rdef,
409            guards: self.guards,
410            external: self.external,
411            case_insensitive: self.case_insensitive,
412            ph: PhantomData,
413        }
414    }
415}
416
417impl<St, In, Out, M, F> ScopeServices<St, In, Out, M, F>
418where
419    St: State,
420    In: 'static,
421    Out: 'static,
422    F: ServiceFactory<
423            St,
424            WebRequest<In>,
425            Res = WebRequest<Out>,
426            Error = WebError<St, St::Error>,
427            InitError = Failure,
428        >,
429{
430    /// Registers another web service inside this scope.
431    ///
432    /// This has the same behavior as [`Scope::service()`]. The service's path
433    /// is joined with the scope prefix and added to the scope's nested router.
434    ///
435    /// If no nested service matches, the scope's default service is used.
436    #[must_use]
437    pub fn service(mut self, factory: impl WebServiceFactory<St, Out>) -> Self {
438        self.services
439            .push(Box::new(ServiceFactoryWrapper::new(factory)));
440        self
441    }
442
443    /// Register a route for a path relative to this scope.
444    ///
445    /// This is shorthand for creating a [`Resource`] with one route and
446    /// registering it with [`ScopeServices::service()`]. The route's method and
447    /// custom guards are promoted to resource guards.
448    ///
449    /// Each call creates a separate resource, so the same relative path can be
450    /// registered more than once with different guards.
451    ///
452    /// ```rust
453    /// use ntex::web::{self, App, HttpResponse};
454    ///
455    /// App::default().service(
456    ///     web::scope("/api")
457    ///         .route("/items", web::get().to(async || "list"))
458    ///         .route(
459    ///             "/items",
460    ///             web::post().to(async || HttpResponse::Created()),
461    ///         )
462    /// );
463    /// ```
464    #[must_use]
465    pub fn route(self, path: &str, mut route: Route<St, Out>) -> Self {
466        self.service(
467            Resource::new(path)
468                .add_guards(route.take_guards())
469                .route(route),
470        )
471    }
472
473    /// Set the fallback service for unmatched requests within this scope.
474    ///
475    /// The fallback is called after the scope prefix and guards match but no
476    /// nested resource matches. Without a custom fallback, the scope returns
477    /// `404 Not Found`; it does not delegate to the application's fallback.
478    /// Routing failures inside a matched resource are handled by that
479    /// resource's fallback instead.
480    ///
481    /// ```rust
482    /// use ntex::web::{self, App, HttpRequest, HttpResponse};
483    ///
484    /// async fn not_found(req: HttpRequest) -> HttpResponse {
485    ///     HttpResponse::NotFound()
486    ///         .body(format!("No API resource for {}", req.path()))
487    /// }
488    ///
489    /// App::default().service(
490    ///     web::scope("/api")
491    ///         .route("/health", web::get().to(async || "ready"))
492    ///         .default_service(web::to(not_found))
493    /// );
494    /// ```
495    #[must_use]
496    pub fn default_service<Sf>(
497        mut self,
498        f: impl IntoServiceFactory<Sf, St, WebRequest<Out>>,
499    ) -> Self
500    where
501        Sf: ServiceFactory<St, WebRequest<Out>, Res = WebResponse> + 'static,
502        Sf::Error: WebResponseError<St, St::Error>,
503        Sf::InitError: IntoFailure,
504    {
505        // create and configure default resource
506        self.default = Some(boxed::factory(
507            f.into_factory()
508                .map_err(WebError::from_err)
509                .map_init_err(IntoFailure::fail),
510        ));
511
512        self
513    }
514}
515
516impl<St, Outer, In, Out, M, F> WebServiceFactory<St, Outer> for ScopeServices<St, In, Out, M, F>
517where
518    St: State,
519    Outer: 'static,
520    In: 'static,
521    Out: 'static,
522    F: ServiceFactory<
523            St,
524            WebRequest<In>,
525            Res = WebRequest<Out>,
526            Error = WebError<St, St::Error>,
527            InitError = Failure,
528        > + 'static,
529    M: Middleware<WebServiceRouter<St, In, Out, F::Service>, St> + 'static,
530    M::Service: Service<St, WebRequest<Outer>, Res = WebResponse, Error = WebError<St, St::Error>>,
531{
532    fn register(mut self, config: &mut WebServiceConfig<St, Outer>) {
533        // Default service
534        let default = self.default.unwrap_or_else(|| {
535            boxed::factory(
536                factory(async move |req: WebRequest<Out>| {
537                    Ok(req.into_response(Response::NotFound().build()))
538                })
539                .map_init_err(|_| unreachable!()),
540            )
541        });
542
543        // Web app config
544        let mut cfg = WebServiceConfig::new();
545
546        // register nested services
547        for mut svc in self.services {
548            svc.register(&mut cfg);
549        }
550
551        // ResourceMap tree
552        let slash = self.rdef.iter().any(|s| s.ends_with('/'));
553        let mut rmap = ResourceMap::new(ResourceDef::root_prefix(self.rdef.clone()));
554
555        for mut rdef in std::mem::take(&mut self.external) {
556            rmap.add(&mut rdef, None);
557        }
558
559        // Complete scope pipeline creation
560        let services: Vec<_> = cfg
561            .into_services()
562            .into_iter()
563            .map(|(rdef, srv, guards, nested)| {
564                // case for scope prefix ends with '/' and
565                // resource is empty pattern
566                let mut rdef = if slash && rdef.pattern() == "" {
567                    ResourceDef::new("/")
568                } else {
569                    rdef
570                };
571                rmap.add(&mut rdef, nested);
572                (rdef, srv, RefCell::new(guards))
573            })
574            .collect();
575
576        // Create router
577        let mut router = Router::builder();
578        if self.case_insensitive {
579            router.case_insensitive();
580        }
581        for (path, factory, guards) in services {
582            router
583                .resource(path.clone(), factory)
584                .set_check_value(guards.borrow_mut().take());
585        }
586
587        // register final service
588        config.register_service(
589            ResourceDef::root_prefix(self.rdef),
590            if self.guards.is_empty() {
591                None
592            } else {
593                Some(self.guards)
594            },
595            Some(Rc::new(rmap)),
596            ScopeServiceFactory {
597                default,
598                middleware: self.middleware,
599                filter: self.filter,
600                router: Rc::new(router.build()),
601            },
602        );
603    }
604}
605
606/// Scope service
607struct ScopeServiceFactory<St: State, In, Out, M, F> {
608    middleware: M,
609    filter: ServiceChainFactory<F, St, WebRequest<In>>,
610    router: Rc<Router<HttpService<St, Out>, Guards>>,
611    default: HttpService<St, Out>,
612}
613
614impl<St, Outer, In, Out, M, F> ServiceFactory<St, WebRequest<Outer>>
615    for ScopeServiceFactory<St, In, Out, M, F>
616where
617    St: State,
618    Outer: 'static,
619    In: 'static,
620    Out: 'static,
621    F: ServiceFactory<
622            St,
623            WebRequest<In>,
624            Res = WebRequest<Out>,
625            Error = WebError<St, St::Error>,
626            InitError = Failure,
627        > + 'static,
628    M: Middleware<WebServiceRouter<St, In, Out, F::Service>, St> + 'static,
629    M::Service: Service<St, WebRequest<Outer>, Res = WebResponse, Error = WebError<St, St::Error>>,
630{
631    type Res = WebResponse;
632    type Error = WebError<St, St::Error>;
633
634    type Service = M::Service;
635    type InitError = Failure;
636
637    async fn create(&self, st: &St) -> Result<Self::Service, Self::InitError> {
638        let filter = self.filter.create(st).await?;
639
640        // router service
641        Ok(self.middleware.create(
642            st,
643            WebServiceRouter::new(filter, self.router.clone(), self.default.clone()),
644        ))
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use std::convert::Infallible;
651
652    use crate::http::body::{Body, ResponseBody};
653    use crate::http::header::{CONTENT_TYPE, HeaderValue};
654    use crate::http::{Method, StatusCode};
655    use crate::util::Bytes;
656    use crate::web::middleware::DefaultHeaders;
657    use crate::web::test::{TestRequest, call_service, init_service, read_body};
658    use crate::web::{self, App, HttpRequest, HttpResponse, WebRequest, guard};
659
660    #[crate::rt_test]
661    async fn test_scope() {
662        let srv = init_service(
663            App::new()
664                .service(
665                    web::scope("/app")
666                        .service(web::resource("/path1").to(async || HttpResponse::Ok())),
667                )
668                .service(
669                    web::scope("/app2")
670                        .case_insensitive_routing()
671                        .service(web::resource("/path1").to(async || HttpResponse::Ok())),
672                ),
673        )
674        .await;
675
676        let req = TestRequest::with_uri("/app/path1").to_request();
677        let resp = srv.call(req).await.unwrap();
678        assert_eq!(resp.status(), StatusCode::OK);
679
680        let req = TestRequest::with_uri("/app/path10").to_request();
681        let resp = srv.call(req).await.unwrap();
682        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
683
684        let req = TestRequest::with_uri("/app2/path1").to_request();
685        let resp = srv.call(req).await.unwrap();
686        assert_eq!(resp.status(), StatusCode::OK);
687
688        let req = TestRequest::with_uri("/app2/Path1").to_request();
689        let resp = srv.call(req).await.unwrap();
690        assert_eq!(resp.status(), StatusCode::OK);
691    }
692
693    #[crate::rt_test]
694    async fn test_scope_root() {
695        let srv = init_service(
696            App::new().service(
697                web::scope("/app")
698                    .service(web::resource("").to(async || HttpResponse::Ok()))
699                    .service(web::resource("/").to(async || HttpResponse::Created())),
700            ),
701        )
702        .await;
703
704        let req = TestRequest::with_uri("/app").to_request();
705        let resp = srv.call(req).await.unwrap();
706        assert_eq!(resp.status(), StatusCode::OK);
707
708        let req = TestRequest::with_uri("/app/").to_request();
709        let resp = srv.call(req).await.unwrap();
710        assert_eq!(resp.status(), StatusCode::CREATED);
711    }
712
713    #[crate::rt_test]
714    async fn test_scope_root_multi() {
715        let srv = init_service(
716            App::new().service(
717                web::scope(["/app", "/app2"])
718                    .service(web::resource("").to(async || HttpResponse::Ok()))
719                    .service(web::resource("/").to(async || HttpResponse::Created())),
720            ),
721        )
722        .await;
723
724        for url in &["/app", "/app2"] {
725            let req = TestRequest::with_uri(url).to_request();
726            let resp = srv.call(req).await.unwrap();
727            assert_eq!(resp.status(), StatusCode::OK);
728        }
729
730        for url in &["/app/", "/app2/"] {
731            let req = TestRequest::with_uri(url).to_request();
732            let resp = srv.call(req).await.unwrap();
733            assert_eq!(resp.status(), StatusCode::CREATED);
734        }
735    }
736
737    #[crate::rt_test]
738    async fn test_scope_root2() {
739        let srv = init_service(App::new().service(
740            web::scope("/app/").service(web::resource("").to(async || HttpResponse::Ok())),
741        ))
742        .await;
743
744        let req = TestRequest::with_uri("/app").to_request();
745        let resp = srv.call(req).await.unwrap();
746        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
747
748        let req = TestRequest::with_uri("/app/").to_request();
749        let resp = srv.call(req).await.unwrap();
750        assert_eq!(resp.status(), StatusCode::OK);
751    }
752
753    #[crate::rt_test]
754    async fn test_scope_root2_multi() {
755        let srv = init_service(
756            App::new().service(
757                web::scope(["/app/", "/app2/"])
758                    .service(web::resource("").to(async || HttpResponse::Ok())),
759            ),
760        )
761        .await;
762
763        for url in &["/app", "/app2"] {
764            let req = TestRequest::with_uri(url).to_request();
765            let resp = srv.call(req).await.unwrap();
766            assert_eq!(resp.status(), StatusCode::NOT_FOUND);
767        }
768
769        for url in &["/app/", "/app2/"] {
770            let req = TestRequest::with_uri(url).to_request();
771            let resp = srv.call(req).await.unwrap();
772            assert_eq!(resp.status(), StatusCode::OK);
773        }
774    }
775
776    #[crate::rt_test]
777    async fn test_scope_root3() {
778        let srv = init_service(App::new().service(
779            web::scope("/app/").service(web::resource("/").to(async || HttpResponse::Ok())),
780        ))
781        .await;
782
783        let req = TestRequest::with_uri("/app").to_request();
784        let resp = srv.call(req).await.unwrap();
785        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
786
787        let req = TestRequest::with_uri("/app/").to_request();
788        let resp = srv.call(req).await.unwrap();
789        assert_eq!(resp.status(), StatusCode::OK);
790    }
791
792    #[crate::rt_test]
793    async fn test_scope_route() {
794        let srv = init_service(
795            App::new().service(
796                web::scope("app")
797                    .route("/path1", web::get().to(async || HttpResponse::Ok()))
798                    .route("/path1", web::delete().to(async || HttpResponse::Ok())),
799            ),
800        )
801        .await;
802
803        for (m, status) in &[
804            (Method::GET, StatusCode::OK),
805            (Method::DELETE, StatusCode::OK),
806            (Method::POST, StatusCode::NOT_FOUND),
807        ] {
808            let req = TestRequest::with_uri("/app/path1")
809                .method(m.clone())
810                .to_request();
811            let resp = srv.call(req).await.unwrap();
812            assert_eq!(resp.status(), status.clone());
813        }
814    }
815
816    #[crate::rt_test]
817    async fn test_scope_route_multi() {
818        let srv = init_service(
819            App::new().service(
820                web::scope(["app", "app2"])
821                    .route("/path1", web::get().to(async || HttpResponse::Ok()))
822                    .route("/path1", web::delete().to(async || HttpResponse::Ok())),
823            ),
824        )
825        .await;
826
827        for (m, status) in &[
828            (Method::GET, StatusCode::OK),
829            (Method::DELETE, StatusCode::OK),
830            (Method::POST, StatusCode::NOT_FOUND),
831        ] {
832            let req = TestRequest::with_uri("/app/path1")
833                .method(m.clone())
834                .to_request();
835            let resp = srv.call(req).await.unwrap();
836            assert_eq!(resp.status(), status.clone());
837
838            let req = TestRequest::with_uri("/app2/path1")
839                .method(m.clone())
840                .to_request();
841            let resp = srv.call(req).await.unwrap();
842            assert_eq!(resp.status(), status.clone());
843        }
844    }
845
846    #[crate::rt_test]
847    async fn test_scope_route_without_leading_slash() {
848        let srv = init_service(
849            App::new().service(
850                web::scope("app").service(
851                    web::resource("path1")
852                        .route(web::get().to(async || HttpResponse::Ok()))
853                        .route(web::delete().to(async || HttpResponse::Ok())),
854                ),
855            ),
856        )
857        .await;
858
859        let req = TestRequest::with_uri("/app/path1").to_request();
860        let resp = srv.call(req).await.unwrap();
861        assert_eq!(resp.status(), StatusCode::OK);
862
863        let req = TestRequest::with_uri("/app/path1")
864            .method(Method::DELETE)
865            .to_request();
866        let resp = srv.call(req).await.unwrap();
867        assert_eq!(resp.status(), StatusCode::OK);
868
869        let req = TestRequest::with_uri("/app/path1")
870            .method(Method::POST)
871            .to_request();
872        let resp = srv.call(req).await.unwrap();
873        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
874    }
875
876    #[crate::rt_test]
877    async fn test_scope_guard() {
878        let srv = init_service(
879            App::new()
880                .service(
881                    web::scope("/app")
882                        .guard(guard::Get())
883                        .service(web::resource("/path1").to(async || HttpResponse::Ok())),
884                )
885                .service(
886                    web::scope("/app")
887                        .guard(guard::Post())
888                        .service(web::resource("/path1").to(async || HttpResponse::NotModified())),
889                )
890                .service(web::resource("/app/path1").to(async || HttpResponse::NoContent())),
891        )
892        .await;
893
894        let req = TestRequest::with_uri("/app/path1")
895            .method(Method::POST)
896            .to_request();
897        let resp = srv.call(req).await.unwrap();
898        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
899
900        let req = TestRequest::with_uri("/app/path1")
901            .method(Method::GET)
902            .to_request();
903        let resp = srv.call(req).await.unwrap();
904        assert_eq!(resp.status(), StatusCode::OK);
905
906        let req = TestRequest::with_uri("/app/path1")
907            .method(Method::DELETE)
908            .to_request();
909        let resp = srv.call(req).await.unwrap();
910        assert_eq!(resp.status(), StatusCode::NO_CONTENT);
911    }
912
913    #[crate::rt_test]
914    async fn test_scope_variable_segment() {
915        let srv = init_service(App::new().service(web::scope("/ab-{project}").service(
916            web::resource("/path1").to(async move |r: HttpRequest| {
917                HttpResponse::Ok().body(format!("project: {}", &r.match_info()["project"]))
918            }),
919        )))
920        .await;
921
922        let req = TestRequest::with_uri("/ab-project1/path1").to_request();
923        let resp = srv.call(req).await.unwrap();
924        assert_eq!(resp.status(), StatusCode::OK);
925
926        if let ResponseBody::Body(Body::Bytes(b)) = resp.body() {
927            let bytes: Bytes = b.clone();
928            assert_eq!(bytes, Bytes::from_static(b"project: project1"));
929        }
930
931        let req = TestRequest::with_uri("/aa-project1/path1").to_request();
932        let resp = srv.call(req).await.unwrap();
933        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
934    }
935
936    #[crate::rt_test]
937    async fn test_scope_variable_segment2() {
938        let srv = init_service(App::new().service(web::scope("/ab-{project}").service(
939            web::resource(["", "/"]).to(async move |r: HttpRequest| {
940                HttpResponse::Ok().body(format!("project: {}", &r.match_info()["project"]))
941            }),
942        )))
943        .await;
944
945        let req = TestRequest::with_uri("/ab-project1").to_request();
946        let resp = srv.call(req).await.unwrap();
947        assert_eq!(resp.status(), StatusCode::OK);
948
949        if let ResponseBody::Body(Body::Bytes(b)) = resp.body() {
950            let bytes: Bytes = b.clone();
951            assert_eq!(bytes, Bytes::from_static(b"project: project1"));
952        }
953
954        let req = TestRequest::with_uri("/ab-project1/").to_request();
955        let resp = srv.call(req).await.unwrap();
956        assert_eq!(resp.status(), StatusCode::OK);
957
958        if let ResponseBody::Body(Body::Bytes(b)) = resp.body() {
959            let bytes: Bytes = b.clone();
960            assert_eq!(bytes, Bytes::from_static(b"project: project1"));
961        }
962
963        let req = TestRequest::with_uri("/aa-project1").to_request();
964        let resp = srv.call(req).await.unwrap();
965        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
966    }
967
968    #[crate::rt_test]
969    async fn test_nested_scope() {
970        let srv = init_service(App::new().service(web::scope("/app").service(
971            web::scope("/t1").service(web::resource("/path1").to(async || HttpResponse::Created())),
972        )))
973        .await;
974
975        let req = TestRequest::with_uri("/app/t1/path1").to_request();
976        let resp = srv.call(req).await.unwrap();
977        assert_eq!(resp.status(), StatusCode::CREATED);
978    }
979
980    #[crate::rt_test]
981    async fn test_nested_scope_no_slash() {
982        let srv = init_service(App::new().service(web::scope("/app").service(
983            web::scope("t1").service(web::resource("/path1").to(async || HttpResponse::Created())),
984        )))
985        .await;
986
987        let req = TestRequest::with_uri("/app/t1/path1").to_request();
988        let resp = srv.call(req).await.unwrap();
989        assert_eq!(resp.status(), StatusCode::CREATED);
990    }
991
992    #[crate::rt_test]
993    async fn test_nested_scope_root() {
994        let srv = init_service(
995            App::new().service(
996                web::scope("/app").service(
997                    web::scope("/t1")
998                        .service(web::resource("").to(async || HttpResponse::Ok()))
999                        .service(web::resource("/").to(async || HttpResponse::Created())),
1000                ),
1001            ),
1002        )
1003        .await;
1004
1005        let req = TestRequest::with_uri("/app/t1").to_request();
1006        let resp = srv.call(req).await.unwrap();
1007        assert_eq!(resp.status(), StatusCode::OK);
1008
1009        let req = TestRequest::with_uri("/app/t1/").to_request();
1010        let resp = srv.call(req).await.unwrap();
1011        assert_eq!(resp.status(), StatusCode::CREATED);
1012    }
1013
1014    #[crate::rt_test]
1015    async fn test_nested_scope_filter() {
1016        let srv = init_service(
1017            App::new().service(
1018                web::scope("/app").service(
1019                    web::scope("/t1")
1020                        .guard(guard::Get())
1021                        .service(web::resource("/path1").to(async || HttpResponse::Ok())),
1022                ),
1023            ),
1024        )
1025        .await;
1026
1027        let req = TestRequest::with_uri("/app/t1/path1")
1028            .method(Method::POST)
1029            .to_request();
1030        let resp = srv.call(req).await.unwrap();
1031        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1032
1033        let req = TestRequest::with_uri("/app/t1/path1")
1034            .method(Method::GET)
1035            .to_request();
1036        let resp = srv.call(req).await.unwrap();
1037        assert_eq!(resp.status(), StatusCode::OK);
1038    }
1039
1040    #[crate::rt_test]
1041    async fn test_nested_scope_with_variable_segment() {
1042        let srv = init_service(App::new().service(web::scope("/app").service(
1043            web::scope("/{project_id}").service(web::resource("/path1").to(
1044                async move |r: HttpRequest| {
1045                    HttpResponse::Created()
1046                        .body(format!("project: {}", &r.match_info()["project_id"]))
1047                },
1048            )),
1049        )))
1050        .await;
1051
1052        let req = TestRequest::with_uri("/app/project_1/path1").to_request();
1053        let resp = srv.call(req).await.unwrap();
1054        assert_eq!(resp.status(), StatusCode::CREATED);
1055
1056        if let ResponseBody::Body(Body::Bytes(b)) = resp.body() {
1057            let bytes: Bytes = b.clone();
1058            assert_eq!(bytes, Bytes::from_static(b"project: project_1"));
1059        }
1060    }
1061
1062    #[crate::rt_test]
1063    async fn test_nested2_scope_with_variable_segment() {
1064        let srv = init_service(App::new().service(web::scope("/app").service(
1065            web::scope("/{project}").service(web::scope("/{id}").service(
1066                web::resource("/path1").to(async move |r: HttpRequest| {
1067                    HttpResponse::Created().body(format!(
1068                        "project: {} - {}",
1069                        &r.match_info()["project"],
1070                        &r.match_info()["id"],
1071                    ))
1072                }),
1073            )),
1074        )))
1075        .await;
1076
1077        let req = TestRequest::with_uri("/app/test/1/path1").to_request();
1078        let resp = srv.call(req).await.unwrap();
1079        assert_eq!(resp.status(), StatusCode::CREATED);
1080
1081        if let ResponseBody::Body(Body::Bytes(b)) = resp.body() {
1082            let bytes: Bytes = b.clone();
1083            assert_eq!(bytes, Bytes::from_static(b"project: test - 1"));
1084        }
1085
1086        let req = TestRequest::with_uri("/app/test/1/path2").to_request();
1087        let resp = srv.call(req).await.unwrap();
1088        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1089    }
1090
1091    #[crate::rt_test]
1092    async fn test_default_resource() {
1093        let srv = init_service(
1094            App::new().service(
1095                web::scope("/app")
1096                    .service(web::resource("/path1").to(async || HttpResponse::Ok()))
1097                    .default_service(async move |r: WebRequest<()>| {
1098                        Ok::<_, Infallible>(r.into_response(HttpResponse::BadRequest()))
1099                    }),
1100            ),
1101        )
1102        .await;
1103
1104        let req = TestRequest::with_uri("/app/path2").to_request();
1105        let resp = srv.call(req).await.unwrap();
1106        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1107
1108        let req = TestRequest::with_uri("/path2").to_request();
1109        let resp = srv.call(req).await.unwrap();
1110        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1111    }
1112
1113    #[crate::rt_test]
1114    async fn test_filter() {
1115        let filter = std::rc::Rc::new(std::cell::Cell::new(false));
1116        let filter2 = filter.clone();
1117        let srv = init_service(
1118            App::new().service(
1119                web::scope("app")
1120                    .filter(async move |req: WebRequest<()>| {
1121                        filter2.set(true);
1122                        Ok::<_, Infallible>(req)
1123                    })
1124                    .route("/test", web::get().to(async || HttpResponse::Ok())),
1125            ),
1126        )
1127        .await;
1128        let req = TestRequest::with_uri("/app/test").to_request();
1129        let resp = call_service(&srv, req).await;
1130        assert_eq!(resp.status(), StatusCode::OK);
1131        assert!(filter.get());
1132    }
1133
1134    #[crate::rt_test]
1135    async fn test_middleware() {
1136        let srv = init_service(
1137            App::new().service(
1138                web::scope("app")
1139                    .middleware(
1140                        DefaultHeaders::new()
1141                            .header(CONTENT_TYPE, HeaderValue::from_static("0001")),
1142                    )
1143                    .service(
1144                        web::resource("/test").route(web::get().to(async || HttpResponse::Ok())),
1145                    ),
1146            ),
1147        )
1148        .await;
1149
1150        let req = TestRequest::with_uri("/app/test").to_request();
1151        let resp = call_service(&srv, req).await;
1152        assert_eq!(resp.status(), StatusCode::OK);
1153        assert_eq!(
1154            resp.headers().get(CONTENT_TYPE).unwrap(),
1155            HeaderValue::from_static("0001")
1156        );
1157    }
1158
1159    #[crate::rt_test]
1160    async fn test_scope_config_2() {
1161        let srv = init_service(App::new().service(web::scope("/app").configure(|s| {
1162            s.service(web::scope("/v1").configure(|s| {
1163                s.route("/", web::get().to(async || HttpResponse::Ok()));
1164            }));
1165        })))
1166        .await;
1167
1168        let req = TestRequest::with_uri("/app/v1/").to_request();
1169        let resp = srv.call(req).await.unwrap();
1170        assert_eq!(resp.status(), StatusCode::OK);
1171    }
1172
1173    #[crate::rt_test]
1174    async fn test_url_for_external() {
1175        let srv = init_service(App::new().service(web::scope("/app").configure(|s| {
1176            s.service(web::scope("/v1").configure(|s| {
1177                s.external_resource("youtube", "https://youtube.com/watch/{video_id}");
1178                s.route(
1179                    "/",
1180                    web::get().to(async move |req: HttpRequest| {
1181                        HttpResponse::Ok().body(
1182                            req.url_for("youtube", ["xxxxxx"])
1183                                .unwrap()
1184                                .as_str()
1185                                .to_string(),
1186                        )
1187                    }),
1188                );
1189            }));
1190        })))
1191        .await;
1192
1193        let req = TestRequest::with_uri("/app/v1/").to_request();
1194        let resp = srv.call(req).await.unwrap();
1195        assert_eq!(resp.status(), StatusCode::OK);
1196        let body = read_body(resp).await;
1197        assert_eq!(body, &b"https://youtube.com/watch/xxxxxx"[..]);
1198    }
1199
1200    #[crate::rt_test]
1201    async fn test_url_for_nested() {
1202        let srv = init_service(App::new().service(web::scope("/a").service(
1203            web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(web::get().to(
1204                async move |req: HttpRequest| {
1205                    HttpResponse::Ok().body(format!("{}", req.url_for("c", ["12345"]).unwrap()))
1206                },
1207            ))),
1208        )))
1209        .await;
1210
1211        let req = TestRequest::with_uri("/a/b/c/test").to_request();
1212        let resp = call_service(&srv, req).await;
1213        assert_eq!(resp.status(), StatusCode::OK);
1214        let body = read_body(resp).await;
1215        assert_eq!(
1216            body,
1217            Bytes::from_static(b"http://localhost:8080/a/b/c/12345")
1218        );
1219    }
1220
1221    #[crate::rt_test]
1222    async fn test_url_for_parent() {
1223        async fn urls(req: HttpRequest) -> HttpResponse {
1224            let index = req.url_for("index", ["1"]).unwrap();
1225            let youtube = req.url_for("youtube", ["2"]).unwrap();
1226            let sibling = req.url_for("sibling", ["3"]).unwrap();
1227            HttpResponse::Ok().body(format!("{index} {youtube} {sibling}"))
1228        }
1229
1230        let srv = init_service(
1231            App::new()
1232                .external_resource("youtube", "https://youtube.com/watch/{video_id}")
1233                .service(
1234                    web::resource("/index/{id}")
1235                        .name("index")
1236                        .to(async || HttpResponse::Ok()),
1237                )
1238                .service(
1239                    web::scope("/sibling").service(
1240                        web::resource("/{id}")
1241                            .name("sibling")
1242                            .to(async || HttpResponse::Ok()),
1243                    ),
1244                )
1245                .service(
1246                    web::scope("/a")
1247                        .service(web::resource("/").to(urls))
1248                        .service(web::scope("/b").service(web::resource("/").to(urls))),
1249                ),
1250        )
1251        .await;
1252
1253        for uri in ["/a/", "/a/b/"] {
1254            let req = TestRequest::with_uri(uri).to_request();
1255            let resp = call_service(&srv, req).await;
1256            assert_eq!(resp.status(), StatusCode::OK);
1257            let body = read_body(resp).await;
1258            assert_eq!(
1259                body,
1260                Bytes::from_static(
1261                    b"http://localhost:8080/index/1 https://youtube.com/watch/2 http://localhost:8080/sibling/3"
1262                ),
1263                "{uri}"
1264            );
1265        }
1266    }
1267
1268    #[crate::rt_test]
1269    async fn test_scope_default_service_only() {
1270        let srv = init_service(App::new().service(web::scope("/app").default_service(
1271            async |r: WebRequest<()>| Ok::<_, Infallible>(r.into_response(HttpResponse::Created())),
1272        )))
1273        .await;
1274        let resp = call_service(&srv, TestRequest::with_uri("/app/path").to_request()).await;
1275        assert_eq!(resp.status(), StatusCode::CREATED);
1276        let resp = call_service(&srv, TestRequest::with_uri("/other").to_request()).await;
1277        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1278    }
1279}