ntex/web/app.rs
1#![allow(clippy::new_without_default)]
2use std::marker::PhantomData;
3
4use crate::error::{Failure, IntoFailure};
5use crate::http::{Request, Response};
6use crate::router::ResourceDef;
7use crate::service::{Identity, ServiceChainFactory, map_state_factory};
8use crate::{Cfg, IntoServiceFactory, Middleware, Service, ServiceFactory, factory};
9
10use super::app_service::{AppFactory, WebServiceRouter};
11use super::config::{ServiceConfig, WebAppConfig};
12use super::error::{WebError, WebResponseError};
13use super::service::{AppServiceFactory, ServiceFactoryWrapper, WebServiceFactory};
14use super::stack::{Filter, WebStack};
15use super::{HttpService, Resource, Route, State, WebRequest, WebResponse};
16
17/// The main builder for a web application.
18///
19/// Start with `App::new()` then add routes, resources,
20/// scopes, middleware, filters, application state, and a fallback response.
21///
22/// Middleware and filters run before routing. ntex then chooses the resource
23/// or scope that matches the request. If nothing matches, the application
24/// returns `404 Not Found` unless you provide a custom fallback.
25///
26/// Add application-wide settings, such as middleware, filters,
27/// [`App::with_config()`], and case-insensitive routing, before adding the
28/// first route or service. After that, the builder becomes [`AppServices`],
29/// where you can continue adding routes and services.
30///
31/// ```rust
32/// use ntex::web::{self, middleware, App, HttpResponse};
33///
34/// App::default()
35/// .middleware(middleware::Logger::default())
36/// .service(
37/// web::resource("/users")
38/// .route(web::get().to(async || "users"))
39/// .route(web::post().to(async || HttpResponse::Created())),
40/// )
41/// .default_service(
42/// web::to(async || HttpResponse::NotFound().body("Not found")),
43/// );
44/// ```
45#[derive(derive_more::Debug)]
46#[debug("App")]
47pub struct App<St: State, In = (), Out = In, M = Identity, F = Filter<St, In>> {
48 middleware: M,
49 filter: ServiceChainFactory<F, St, WebRequest<In>>,
50 external: Vec<ResourceDef>,
51 config: Option<Cfg<WebAppConfig>>,
52 case_insensitive: bool,
53 ph: PhantomData<Out>,
54}
55
56/// An application builder that already has routing configuration.
57///
58/// You usually do not need to name this type. It is returned naturally after
59/// calling [`App::route()`], [`App::service()`], [`App::configure()`], or
60/// [`App::default_service()`].
61///
62/// From here, you can add more routes and services or set the application
63/// fallback. Return the finished builder from the factory passed to
64/// [`web::server()`]. The [`AppServices::build()`] and
65/// [`AppServices::build_with()`] methods are available when you need to
66/// connect the application to a lower-level HTTP service yourself.
67///
68/// [`web::server()`]: super::server
69#[derive(derive_more::Debug)]
70#[debug("AppServices")]
71pub struct AppServices<St: State, In, Out, M, F> {
72 middleware: M,
73 filter: ServiceChainFactory<F, St, WebRequest<In>>,
74 services: Vec<Box<dyn AppServiceFactory<St, Out>>>,
75 default: Option<HttpService<St, Out>>,
76 external: Vec<ResourceDef>,
77 config: Option<Cfg<WebAppConfig>>,
78 case_insensitive: bool,
79 ph: PhantomData<In>,
80}
81
82impl Default for App<()> {
83 fn default() -> Self {
84 App {
85 middleware: Identity,
86 filter: factory(Filter::new()),
87 config: None,
88 external: Vec::new(),
89 case_insensitive: false,
90 ph: PhantomData,
91 }
92 }
93}
94
95impl<St: State, In> App<St, In, In> {
96 /// Create application builder. Application can be configured with a builder-like pattern.
97 #[must_use]
98 pub fn new() -> Self {
99 App {
100 middleware: Identity,
101 filter: factory(Filter::new()),
102 config: None,
103 external: Vec::new(),
104 case_insensitive: false,
105 ph: PhantomData,
106 }
107 }
108}
109
110impl<St, In, Out, M, F> App<St, In, Out, M, F>
111where
112 St: State,
113 In: 'static,
114 Out: 'static,
115 F: ServiceFactory<
116 St,
117 WebRequest<In>,
118 Res = WebRequest<Out>,
119 Error = WebError<St, St::Error>,
120 InitError = Failure,
121 >,
122{
123 /// Run external configuration as part of the application building
124 /// process.
125 ///
126 /// This function is useful for moving parts of configuration to a
127 /// different module or even library. For example,
128 /// some of the resource's configuration could be moved to different module.
129 ///
130 /// ```rust,ignore
131 /// use ntex::web::{self, middleware, App, HttpResponse};
132 ///
133 /// // this function could be located in different module
134 /// fn config(cfg: &mut web::ServiceConfig) {
135 /// cfg.service(web::resource("/test")
136 /// .route(web::get().to(async || { HttpResponse::Ok() }))
137 /// .route(web::head().to(async || { HttpResponse::MethodNotAllowed() }))
138 /// );
139 /// }
140 ///
141 /// fn main() {
142 /// let app = App::default()
143 /// .middleware(middleware::Logger::default())
144 /// .configure(config) // <- register resources
145 /// .route("/index.html", web::get().to(async || { HttpResponse::Ok() }));
146 /// }
147 /// ```
148 #[must_use]
149 pub fn configure(
150 self,
151 f: impl FnOnce(&mut ServiceConfig<St, Out>),
152 ) -> AppServices<St, In, Out, M, F> {
153 let mut cfg = ServiceConfig::new(self.external);
154 f(&mut cfg);
155
156 AppServices {
157 services: cfg.services,
158 default: None,
159 filter: self.filter,
160 middleware: self.middleware,
161 config: self.config,
162 external: cfg.external,
163 case_insensitive: self.case_insensitive,
164 ph: PhantomData,
165 }
166 }
167
168 /// Register a route for an application path.
169 ///
170 /// This is shorthand for creating a [`Resource`] with one route and
171 /// registering it with [`App::service()`]. The route's method and custom
172 /// guards are promoted to resource guards.
173 ///
174 /// Each call creates a separate resource, so the same path can be
175 /// registered more than once with different guards.
176 ///
177 /// ```rust
178 /// use ntex::web::{self, App, HttpResponse};
179 ///
180 /// App::default()
181 /// .route("/items", web::get().to(async || "list"))
182 /// .route("/items", web::post().to(async || HttpResponse::Created()));
183 /// ```
184 #[must_use]
185 pub fn route(self, path: &str, mut route: Route<St, Out>) -> AppServices<St, In, Out, M, F> {
186 self.service(
187 Resource::new(path)
188 .add_guards(route.take_guards())
189 .route(route),
190 )
191 }
192
193 /// Registers a web service with the application.
194 ///
195 /// A service defines its own path and guards through [`WebServiceFactory`].
196 /// Common services include [`Resource`], [`Scope`](crate::web::Scope), handlers created with
197 /// route attribute macros, and custom services built with `web::service()`.
198 ///
199 /// Use a resource to group several routes, filters, middleware, or a
200 /// fallback under one path. Use a scope to group services under a shared
201 /// path prefix.
202 ///
203 /// If no registered service matches the request path and guards, the
204 /// application's default service is used.
205 ///
206 /// ```rust
207 /// use ntex::web::{self, App, HttpResponse};
208 ///
209 /// App::default()
210 /// .service(
211 /// web::resource("/users")
212 /// .route(web::get().to(async || "users"))
213 /// .route(web::post().to(async || HttpResponse::Created())),
214 /// )
215 /// .service(
216 /// web::scope("/api")
217 /// .route("/health", web::get().to(async || "OK")),
218 /// );
219 /// ```
220 #[must_use]
221 pub fn service<S>(self, factory: S) -> AppServices<St, In, Out, M, F>
222 where
223 S: WebServiceFactory<St, Out> + 'static,
224 {
225 AppServices {
226 services: vec![Box::new(ServiceFactoryWrapper::new(factory))],
227 default: None,
228 filter: self.filter,
229 middleware: self.middleware,
230 config: self.config,
231 external: self.external,
232 case_insensitive: self.case_insensitive,
233 ph: PhantomData,
234 }
235 }
236
237 /// Set the fallback service for unmatched application requests.
238 ///
239 /// The fallback is called when no top-level resource or scope matches the
240 /// request path and guards. Without a custom fallback, the application
241 /// returns `404 Not Found`.
242 ///
243 /// A matched resource or scope handles its own routing failures, so its
244 /// requests do not fall through to this service.
245 ///
246 /// ```rust
247 /// use ntex::web::{self, App, HttpRequest, HttpResponse};
248 ///
249 /// async fn not_found(req: HttpRequest) -> HttpResponse {
250 /// HttpResponse::NotFound()
251 /// .body(format!("No resource for {}", req.path()))
252 /// }
253 ///
254 /// App::default()
255 /// .route("/health", web::get().to(async || "ready"))
256 /// .default_service(web::to(not_found));
257 /// ```
258 #[must_use]
259 pub fn default_service<U>(
260 self,
261 f: impl IntoServiceFactory<U, St, WebRequest<Out>>,
262 ) -> AppServices<St, In, Out, M, F>
263 where
264 U: ServiceFactory<St, WebRequest<Out>, Res = WebResponse> + 'static,
265 U::Error: WebResponseError<St, St::Error>,
266 U::InitError: IntoFailure,
267 {
268 // create and configure default resource
269 let default = Some(HttpService::new(
270 f.into_factory()
271 .map_err(WebError::from_err)
272 .map_init_err(IntoFailure::fail),
273 ));
274
275 AppServices {
276 default,
277 services: Vec::new(),
278 filter: self.filter,
279 middleware: self.middleware,
280 config: self.config,
281 external: self.external,
282 case_insensitive: self.case_insensitive,
283 ph: PhantomData,
284 }
285 }
286
287 /// Register an external resource.
288 ///
289 /// External resources are useful for URL generation purposes only
290 /// and are never considered for matching at request time. Calls to
291 /// `HttpRequest::url_for()` will work as expected.
292 ///
293 /// ```rust
294 /// use ntex::web::{self, App, HttpRequest, HttpResponse, error::UrlGenerationError};
295 ///
296 /// async fn index(req: HttpRequest) -> Result<HttpResponse, UrlGenerationError> {
297 /// let url = req.url_for("youtube", &["asdlkjqme"])?;
298 /// assert_eq!(url.as_str(), "https://youtube.com/watch/asdlkjqme");
299 /// Ok(HttpResponse::Ok().into())
300 /// }
301 ///
302 /// fn main() {
303 /// let app = App::default()
304 /// .external_resource("youtube", "https://youtube.com/watch/{video_id}")
305 /// .service(web::resource("/index.html").route(
306 /// web::get().to(index)));
307 /// }
308 /// ```
309 #[must_use]
310 pub fn external_resource(mut self, name: impl AsRef<str>, url: impl AsRef<str>) -> Self {
311 let mut rdef = ResourceDef::new(url.as_ref());
312 rdef.set_name(name.as_ref());
313 self.external.push(rdef);
314 self
315 }
316
317 /// Set the application's runtime configuration.
318 ///
319 /// [`WebAppConfig`] contains connection metadata used by the application,
320 /// such as the host, secure-connection flag, local address, and request pool
321 /// size. It can also store typed configuration values with
322 /// [`WebAppConfig::set_state()`]; those values are available through
323 /// [`HttpRequest::app_state()`](crate::web::HttpRequest::app_state) and [`WebRequest::app_state()`].
324 ///
325 /// Without an explicit configuration, each request uses the
326 /// [`WebAppConfig`] from its I/O context, or the default configuration if
327 /// the request has no associated I/O object. This method overrides that
328 /// selection for every request handled by this application.
329 ///
330 /// This configuration is separate from the service-level application state
331 /// represented by `St`. To register routes and services from an external
332 /// function, use [`App::configure()`] instead.
333 ///
334 /// ```rust
335 /// use ntex::web::{self, App, HttpRequest, WebAppConfig};
336 ///
337 /// async fn index(req: HttpRequest) -> String {
338 /// let value = req.app_state::<usize>().copied().unwrap_or_default();
339 /// format!("Configured value: {value}")
340 /// }
341 ///
342 /// let config = WebAppConfig::new()
343 /// .set_host("www.example.com".to_owned())
344 /// .set_secure()
345 /// .set_state(42usize);
346 ///
347 /// App::default()
348 /// .with_config(config)
349 /// .route("/", web::get().to(index));
350 /// ```
351 #[must_use]
352 pub fn with_config(mut self, cfg: impl Into<Cfg<WebAppConfig>>) -> Self {
353 self.config = Some(cfg.into());
354 self
355 }
356
357 /// Registers a request filter.
358 ///
359 /// Application filters run before the application router selects a
360 /// resource or scope. Filters are called in registration order, and each
361 /// filter receives the [`WebRequest`] returned by the previous one.
362 ///
363 /// A filter can inspect or modify the request, or use
364 /// [`WebRequest::map_state()`] to change its request-local state type. It
365 /// must return another `WebRequest` to continue processing. Returning an
366 /// error stops the filter chain and prevents routing; the error is handled
367 /// through [`WebResponseError`].
368 ///
369 /// Application middleware wraps the filter and router, so middleware runs
370 /// before filters on the inbound path.
371 ///
372 /// ```rust
373 /// use std::convert::Infallible;
374 /// use ntex::web::{self, App, WebRequest};
375 ///
376 /// async fn authenticate(
377 /// req: WebRequest<()>,
378 /// ) -> Result<WebRequest<&'static str>, Infallible> {
379 /// Ok(req.map_state(|()| "alice"))
380 /// }
381 ///
382 /// async fn index(_state: &(), user: &'static str) -> String {
383 /// format!("Hello, {user}!")
384 /// }
385 ///
386 /// App::new()
387 /// .filter(authenticate)
388 /// .route("/", web::get().to_with_state(index));
389 /// ```
390 #[must_use]
391 pub fn filter<Sf, R>(
392 self,
393 filter: impl IntoServiceFactory<Sf, St, WebRequest<Out>>,
394 ) -> App<
395 St,
396 In,
397 R,
398 M,
399 impl ServiceFactory<
400 St,
401 WebRequest<In>,
402 Res = WebRequest<R>,
403 Error = WebError<St, St::Error>,
404 InitError = Failure,
405 >,
406 >
407 where
408 Sf: ServiceFactory<St, WebRequest<Out>, Res = WebRequest<R>>,
409 Sf::Error: WebResponseError<St, St::Error>,
410 Sf::InitError: IntoFailure,
411 {
412 App {
413 filter: self.filter.and_then(
414 filter
415 .into_factory()
416 .map_err(WebError::from_err)
417 .map_init_err(IntoFailure::fail),
418 ),
419 middleware: self.middleware,
420 config: self.config,
421 external: self.external,
422 case_insensitive: self.case_insensitive,
423 ph: PhantomData,
424 }
425 }
426
427 /// Registers a middleware for this application.
428 ///
429 /// Use application middleware for work that should apply to every request,
430 /// such as logging, response headers, or authentication. It runs before
431 /// the application filter and router on the way in, and can inspect or
432 /// modify the response on the way back.
433 ///
434 /// Middleware may also return a response without calling the service it
435 /// wraps. In that case, the rest of the application pipeline is skipped.
436 ///
437 /// Requests pass through middleware in the order it was added. Responses
438 /// travel back in the opposite order. In this example, `DefaultHeaders`
439 /// sees the request before `Logger`, while `Logger` sees the response
440 /// before `DefaultHeaders`.
441 ///
442 /// Custom middleware should call the wrapped service through
443 /// [`Ctx::call()`](crate::Ctx::call) so readiness and lifecycle events are handled
444 /// correctly.
445 ///
446 /// ```rust
447 /// use ntex::web::{self, middleware, App};
448 ///
449 /// App::default()
450 /// .middleware(
451 /// middleware::DefaultHeaders::new()
452 /// .header("x-application", "example"),
453 /// )
454 /// .middleware(middleware::Logger::default())
455 /// .route("/", web::get().to(async || "Hello"));
456 /// ```
457 #[must_use]
458 pub fn middleware<U>(self, mw: U) -> App<St, In, Out, WebStack<St, U, M>, F> {
459 App {
460 middleware: WebStack::new(mw, self.middleware),
461 filter: self.filter,
462 config: self.config,
463 external: self.external,
464 case_insensitive: self.case_insensitive,
465 ph: PhantomData,
466 }
467 }
468
469 #[must_use]
470 /// Use ascii case-insensitive routing.
471 ///
472 /// Only static segments could be case-insensitive.
473 pub fn case_insensitive_routing(mut self) -> Self {
474 self.case_insensitive = true;
475 self
476 }
477}
478
479impl<St, In, Out, M, F> AppServices<St, In, Out, M, F>
480where
481 St: State,
482 Out: 'static,
483 F: ServiceFactory<
484 St,
485 WebRequest<In>,
486 Res = WebRequest<Out>,
487 Error = WebError<St, St::Error>,
488 InitError = Failure,
489 >,
490{
491 /// Register a route for an application path.
492 ///
493 /// This is shorthand for creating a [`Resource`] with one route and
494 /// registering it with [`AppServices::service()`]. The route's method and
495 /// custom guards are promoted to resource guards.
496 ///
497 /// Consequently, if those guards reject a request, the generated resource
498 /// does not match and the application router continues searching. If
499 /// nothing else matches, the application default service is used. To use a
500 /// resource-level fallback such as the built-in `405 Method Not Allowed`,
501 /// register an explicit [`Resource`] and add routes with
502 /// [`Resource::route()`].
503 ///
504 /// Each call creates a separate resource, so the same path can be
505 /// registered more than once with different guards.
506 ///
507 /// ```rust
508 /// use ntex::web::{self, App, HttpResponse};
509 ///
510 /// App::default()
511 /// .route("/items", web::get().to(async || "list"))
512 /// .route(
513 /// "/items",
514 /// web::post().to(async || HttpResponse::Created()),
515 /// );
516 /// ```
517 #[must_use]
518 pub fn route(self, path: &str, mut route: Route<St, Out>) -> Self {
519 self.service(
520 Resource::new(path)
521 .add_guards(route.take_guards())
522 .route(route),
523 )
524 }
525
526 /// Registers another web service with the application.
527 ///
528 /// This has the same behavior as [`App::service()`]. The service supplies
529 /// its own path and guards and becomes part of the application's top-level
530 /// router.
531 ///
532 /// If none of the registered services match, the application's default
533 /// service is used.
534 #[must_use]
535 pub fn service<S>(mut self, factory: S) -> Self
536 where
537 S: WebServiceFactory<St, Out> + 'static,
538 {
539 self.services
540 .push(Box::new(ServiceFactoryWrapper::new(factory)));
541 self
542 }
543
544 /// Set the fallback service for unmatched application requests.
545 ///
546 /// The fallback is called when no top-level resource or scope matches the
547 /// request path and guards. Without a custom fallback, the application
548 /// returns `404 Not Found`.
549 ///
550 /// A matched resource or scope handles its own routing failures, so its
551 /// requests do not fall through to this service.
552 ///
553 /// ```rust
554 /// use ntex::web::{self, App, HttpRequest, HttpResponse};
555 ///
556 /// async fn not_found(req: HttpRequest) -> HttpResponse {
557 /// HttpResponse::NotFound()
558 /// .body(format!("No resource for {}", req.path()))
559 /// }
560 ///
561 /// App::default()
562 /// .route("/health", web::get().to(async || "ready"))
563 /// .default_service(web::to(not_found));
564 /// ```
565 #[must_use]
566 pub fn default_service<U>(mut self, f: impl IntoServiceFactory<U, St, WebRequest<Out>>) -> Self
567 where
568 U: ServiceFactory<St, WebRequest<Out>, Res = WebResponse> + 'static,
569 U::Error: WebResponseError<St, St::Error>,
570 U::InitError: IntoFailure,
571 {
572 // create and configure default resource
573 self.default = Some(HttpService::new(
574 f.into_factory()
575 .map_err(WebError::from_err)
576 .map_init_err(IntoFailure::fail),
577 ));
578
579 self
580 }
581}
582
583impl<St, In, Out, M, F> AppServices<St, In, Out, M, F>
584where
585 St: State,
586 In: 'static,
587 Out: 'static,
588 F: ServiceFactory<
589 St,
590 WebRequest<In>,
591 Res = WebRequest<Out>,
592 Error = WebError<St, St::Error>,
593 InitError = Failure,
594 >,
595 M: Middleware<WebServiceRouter<St, In, Out, F::Service>, St> + 'static,
596 M::Service: Service<St, WebRequest<()>, Res = WebResponse, Error = WebError<St, St::Error>>,
597{
598 /// Builds the application into a service factory.
599 ///
600 /// The returned factory accepts [`Request`] values and uses application
601 /// state supplied by the surrounding service pipeline. It can be passed to
602 /// [`HttpService`] when building an HTTP server manually.
603 ///
604 /// Applications passed to [`web::server()`] do not normally need an
605 /// explicit call to `build()`.
606 ///
607 /// ```rust
608 /// use ntex::web;
609 ///
610 /// let factory = web::App::default()
611 /// .route("/", web::get().to(async || "Hello"))
612 /// .build();
613 /// ```
614 ///
615 /// [`HttpService`]: crate::http::HttpService
616 /// [`web::server()`]: super::server
617 pub fn build(
618 self,
619 ) -> impl ServiceFactory<
620 St,
621 Request,
622 Res = Response,
623 Error = WebError<St, St::Error>,
624 InitError = Failure,
625 > {
626 IntoServiceFactory::<AppFactory<St, In, Out, M, F>, St, Request>::into_factory(self)
627 }
628}
629
630impl<St, In, Out, M, F> AppServices<St, In, Out, M, F>
631where
632 St: State,
633 In: 'static,
634 Out: 'static,
635 F: ServiceFactory<
636 St,
637 WebRequest<In>,
638 Res = WebRequest<Out>,
639 Error = WebError<St, St::Error>,
640 InitError = Failure,
641 >,
642 M: Middleware<WebServiceRouter<St, In, Out, F::Service>, St> + 'static,
643 M::Service: Service<St, WebRequest<()>, Res = WebResponse, Error = WebError<St, St::Error>>,
644{
645 /// Builds the application with a fixed application state.
646 ///
647 /// Unlike [`AppServices::build()`], which takes its application state from
648 /// the surrounding service pipeline, this method stores `state` in the
649 /// returned factory. Web handlers, filters, middleware, and services use
650 /// this fixed state even when the outer pipeline uses a different state
651 /// type.
652 ///
653 /// ```rust
654 /// use ntex::web;
655 ///
656 /// #[derive(Clone)]
657 /// struct AppState {
658 /// greeting: &'static str,
659 /// }
660 ///
661 /// impl web::State for AppState {
662 /// type Error = web::DefaultError;
663 /// }
664 ///
665 /// async fn index(state: &AppState, _request_state: ()) -> String {
666 /// state.greeting.to_owned()
667 /// }
668 ///
669 /// let app = web::App::<AppState>::new()
670 /// .route("/", web::get().to_with_state(index))
671 /// .build_with::<()>(AppState { greeting: "Hello" });
672 /// ```
673 pub fn build_with<Outer>(
674 self,
675 state: St,
676 ) -> impl ServiceFactory<
677 Outer,
678 Request,
679 Res = Response,
680 Error = WebError<St, St::Error>,
681 InitError = Failure,
682 >
683 where
684 St: Clone,
685 {
686 map_state_factory(
687 state,
688 IntoServiceFactory::<AppFactory<St, In, Out, M, F>, St, Request>::into_factory(self),
689 )
690 }
691}
692
693impl<St, In, Out, M, F> IntoServiceFactory<AppFactory<St, In, Out, M, F>, St, Request>
694 for AppServices<St, In, Out, M, F>
695where
696 St: State,
697 In: 'static,
698 Out: 'static,
699 F: ServiceFactory<
700 St,
701 WebRequest<In>,
702 Res = WebRequest<Out>,
703 Error = WebError<St, St::Error>,
704 InitError = Failure,
705 >,
706 M: Middleware<WebServiceRouter<St, In, Out, F::Service>, St> + 'static,
707 M::Service: Service<St, WebRequest<()>, Res = WebResponse, Error = WebError<St, St::Error>>,
708{
709 fn into_factory(self) -> AppFactory<St, In, Out, M, F> {
710 AppFactory::new(
711 self.middleware,
712 self.filter,
713 self.services,
714 self.default,
715 self.config,
716 self.external,
717 self.case_insensitive,
718 )
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use std::{cell::Cell, convert::Infallible, rc::Rc};
725
726 use super::*;
727 use crate::http::{Method, StatusCode, header, header::HeaderValue};
728 use crate::web::test::{TestRequest, call_service, init_service, read_body};
729 use crate::web::{self, HttpRequest, HttpResponse, middleware::DefaultHeaders};
730
731 #[crate::rt_test]
732 async fn test_default_resource() {
733 let srv = App::new()
734 .service(web::resource("/test").to(async || HttpResponse::Ok()))
735 .build()
736 .pipeline(())
737 .await
738 .unwrap();
739 let req = TestRequest::with_uri("/test").to_request();
740 let resp = srv.call(req).await.unwrap();
741 assert_eq!(resp.status(), StatusCode::OK);
742
743 let req = TestRequest::with_uri("/blah").to_request();
744 let resp = srv.call(req).await.unwrap();
745 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
746
747 let srv = App::new()
748 .service(web::resource("/test").to(async || HttpResponse::Ok()))
749 .service(
750 web::resource("/test2")
751 .default_service(async move |r: WebRequest<()>| {
752 Ok::<_, Infallible>(r.into_response(HttpResponse::Created()))
753 })
754 .route(web::get().to(async || HttpResponse::Ok())),
755 )
756 .default_service(async move |r: WebRequest<()>| {
757 Ok::<_, Infallible>(r.into_response(HttpResponse::MethodNotAllowed()))
758 })
759 .build()
760 .pipeline(())
761 .await
762 .unwrap();
763
764 let req = TestRequest::with_uri("/blah").to_request();
765 let resp = srv.call(req).await.unwrap();
766 assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
767
768 let req = TestRequest::with_uri("/test2").to_request();
769 let resp = srv.call(req).await.unwrap();
770 assert_eq!(resp.status(), StatusCode::OK);
771
772 let req = TestRequest::with_uri("/test2")
773 .method(Method::POST)
774 .to_request();
775 let resp = srv.call(req).await.unwrap();
776 assert_eq!(resp.status(), StatusCode::CREATED);
777 }
778
779 #[crate::rt_test]
780 async fn test_filter() {
781 let filter = Rc::new(Cell::new(false));
782 let filter2 = filter.clone();
783 let srv = init_service(
784 App::new()
785 .filter(async move |req: WebRequest<()>| {
786 filter2.set(true);
787 Ok::<_, Infallible>(req)
788 })
789 .route("/test", web::get().to(async || HttpResponse::Ok())),
790 )
791 .await;
792 let req = TestRequest::with_uri("/test").to_request();
793 let resp = call_service(&srv, req).await;
794 assert_eq!(resp.status(), StatusCode::OK);
795 assert!(filter.get());
796 }
797
798 #[crate::rt_test]
799 async fn test_wrap() {
800 let srv = init_service(
801 App::new()
802 .middleware(
803 DefaultHeaders::new()
804 .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")),
805 )
806 .route("/test", web::get().to(async || HttpResponse::Ok())),
807 )
808 .await;
809 let req = TestRequest::with_uri("/test").to_request();
810 let resp = call_service(&srv, req).await;
811 assert_eq!(resp.status(), StatusCode::OK);
812 assert_eq!(
813 resp.headers().get(header::CONTENT_TYPE).unwrap(),
814 HeaderValue::from_static("0001")
815 );
816 }
817
818 #[crate::rt_test]
819 async fn test_router_wrap() {
820 let srv = init_service(
821 App::new()
822 .middleware(
823 DefaultHeaders::new()
824 .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")),
825 )
826 .route("/test", web::get().to(async || HttpResponse::Ok())),
827 )
828 .await;
829 let req = TestRequest::with_uri("/test").to_request();
830 let resp = call_service(&srv, req).await;
831 assert_eq!(resp.status(), StatusCode::OK);
832 assert_eq!(
833 resp.headers().get(header::CONTENT_TYPE).unwrap(),
834 HeaderValue::from_static("0001")
835 );
836 }
837
838 #[crate::rt_test]
839 async fn test_case_insensitive_router() {
840 let srv = init_service(
841 App::new()
842 .case_insensitive_routing()
843 .route("/test", web::get().to(async || HttpResponse::Ok())),
844 )
845 .await;
846 let req = TestRequest::with_uri("/test").to_request();
847 let resp = call_service(&srv, req).await;
848 assert_eq!(resp.status(), StatusCode::OK);
849
850 let req = TestRequest::with_uri("/Test").to_request();
851 let resp = call_service(&srv, req).await;
852 assert_eq!(resp.status(), StatusCode::OK);
853 }
854
855 #[crate::rt_test]
856 async fn test_extension() {
857 let cfg = WebAppConfig::new().set_state(10usize);
858
859 let srv = init_service(
860 App::new()
861 .with_config(cfg)
862 .filter(async move |req: WebRequest<()>| {
863 assert_eq!(*req.app_state::<usize>().unwrap(), 10);
864 Ok::<_, Infallible>(req)
865 })
866 .service(web::resource("/").to(async move |req: HttpRequest| {
867 assert_eq!(*req.app_state::<usize>().unwrap(), 10);
868 HttpResponse::Ok()
869 })),
870 )
871 .await;
872
873 let req = TestRequest::default().to_request();
874 let resp = srv.call(req).await.unwrap();
875 assert_eq!(resp.status(), StatusCode::OK);
876 }
877
878 #[crate::rt_test]
879 async fn test_external_resource() {
880 use crate::util::Bytes;
881
882 let srv = init_service(
883 App::new()
884 .external_resource("youtube", "https://youtube.com/watch/{video_id}")
885 .route(
886 "/test",
887 web::get().to(async move |req: HttpRequest| {
888 HttpResponse::Ok()
889 .body(format!("{}", req.url_for("youtube", ["12345"]).unwrap()))
890 }),
891 ),
892 )
893 .await;
894 let req = TestRequest::with_uri("/test").to_request();
895 let resp = call_service(&srv, req).await;
896 assert_eq!(resp.status(), StatusCode::OK);
897 let body = read_body(resp).await;
898 assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
899 }
900
901 #[crate::rt_test]
902 async fn test_pooled_request_resource_map() {
903 use crate::util::Bytes;
904
905 async fn url(req: HttpRequest) -> HttpResponse {
906 HttpResponse::Ok().body(format!("{}", req.url_for("docs", ["1"]).unwrap()))
907 }
908
909 let srv1 = init_service(
910 App::new()
911 .external_resource("docs", "https://one.example.com/{page}")
912 .route("/test", web::get().to(url)),
913 )
914 .await;
915 let srv2 = init_service(
916 App::new()
917 .external_resource("docs", "https://two.example.com/{page}")
918 .route("/test", web::get().to(url)),
919 )
920 .await;
921
922 for _ in 0..2 {
923 for (srv, expected) in [
924 (&srv1, &b"https://one.example.com/1"[..]),
925 (&srv2, &b"https://two.example.com/1"[..]),
926 ] {
927 let req = TestRequest::with_uri("/test").to_request();
928 let resp = call_service(srv, req).await;
929 assert_eq!(read_body(resp).await, Bytes::from_static(expected));
930 }
931 }
932 }
933
934 #[crate::rt_test]
935 async fn test_app_default_service() {
936 let srv =
937 init_service(App::default().default_service(web::to(async || HttpResponse::Created())))
938 .await;
939 let resp = call_service(&srv, TestRequest::with_uri("/any").to_request()).await;
940 assert_eq!(resp.status(), StatusCode::CREATED);
941 srv.shutdown().await;
942 }
943
944 #[crate::rt_test]
945 async fn test_app_service_init_errors() {
946 use crate::service::{fn_factory, fn_service};
947
948 let attempts = Rc::new(Cell::new(0));
949 let attempts2 = attempts.clone();
950 let failing = move || {
951 let attempts = attempts2.clone();
952 fn_factory(async move |(): &()| {
953 attempts.set(attempts.get() + 1);
954 if attempts.get() % 2 == 1 {
955 Err(std::io::Error::other("init error"))
956 } else {
957 Ok(fn_service(async |req: WebRequest<()>| {
958 Ok::<_, Infallible>(req.into_response(HttpResponse::Ok()))
959 }))
960 }
961 })
962 };
963
964 let srv = init_service(
965 App::new()
966 .service(web::service("/test").build(failing()))
967 .default_service(failing()),
968 )
969 .await;
970
971 // failed services are not cached, next request creates them again
972 for path in ["/test", "/unknown"] {
973 let resp = call_service(&srv, TestRequest::with_uri(path).to_request()).await;
974 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
975 let resp = call_service(&srv, TestRequest::with_uri(path).to_request()).await;
976 assert_eq!(resp.status(), StatusCode::OK);
977 let resp = call_service(&srv, TestRequest::with_uri(path).to_request()).await;
978 assert_eq!(resp.status(), StatusCode::OK);
979 }
980 assert_eq!(attempts.get(), 4);
981 }
982
983 #[crate::rt_test]
984 async fn test_app_filter_error() {
985 let srv = init_service(
986 App::new()
987 .filter(async |req: WebRequest<()>| {
988 if req.path() == "/denied" {
989 Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
990 } else {
991 Ok(req)
992 }
993 })
994 .route("/{tail}*", web::to(async || HttpResponse::Ok())),
995 )
996 .await;
997 let resp = call_service(&srv, TestRequest::with_uri("/allowed").to_request()).await;
998 assert_eq!(resp.status(), StatusCode::OK);
999 let resp = call_service(&srv, TestRequest::with_uri("/denied").to_request()).await;
1000 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1001 }
1002}