axum_conf/fluent/
builder.rs

1//! Orchestration and router delegation: setup_middleware(), start(), layer(), route(), etc.
2
3use super::router::FluentRouter;
4use super::shutdown::{ShutdownNotifier, ShutdownPhase};
5use crate::Result;
6
7use {
8    axum::{Router, body::Body, routing::Route},
9    http::Request,
10    std::{convert::Infallible, env, net::SocketAddr, time::Duration},
11    tokio::signal,
12    tower::{Layer, Service},
13};
14
15impl<State> FluentRouter<State>
16where
17    State: Clone + Send + Sync + 'static,
18{
19    /// Sets up all standard middleware layers in the correct order.
20    ///
21    /// This is the **recommended way** to configure middleware. It handles the complex
22    /// ordering requirements automatically, ensuring all layers work correctly together.
23    ///
24    /// # When to Use This Method
25    ///
26    /// **Use `setup_middleware()`** for most applications. It provides production-ready
27    /// defaults and handles middleware dependencies automatically.
28    ///
29    /// **Use individual `setup_*` methods** only when you need:
30    /// - Custom middleware ordering
31    /// - Middleware between specific layers
32    /// - Partial middleware stack (though `exclude` config is preferred)
33    ///
34    /// # What It Configures
35    ///
36    /// - Liveness/readiness probes
37    /// - OIDC authentication (if `keycloak` feature enabled)
38    /// - Request deduplication
39    /// - Concurrency limits
40    /// - Payload size limits
41    /// - Compression/decompression
42    /// - Path normalization
43    /// - Sensitive header protection
44    /// - Request ID generation
45    /// - API versioning
46    /// - CORS headers
47    /// - Security headers (Helmet)
48    /// - Logging and tracing
49    /// - Metrics collection (Prometheus)
50    /// - Request timeouts
51    /// - Rate limiting
52    /// - Panic recovery
53    ///
54    /// # Middleware Order
55    ///
56    /// **CRITICAL**: Middleware is processed outside-in for requests and inside-out for responses.
57    /// The **last layer added is the outermost layer** and executes **first** on incoming requests.
58    ///
59    /// The current order (from innermost to outermost):
60    /// 1. **OIDC Authentication** - Check auth after infrastructure layers
61    /// 2. **Deduplication** - Check for duplicate requests
62    /// 3. **Concurrency limit** - Control concurrent processing
63    /// 4. **Max payload size** - Limit body size
64    /// 5. **Compression/Decompression** - Handle encoding
65    /// 6. **Path normalization** - Normalize before routing
66    /// 7. **Sensitive headers** - Filter before logging
67    /// 8. **API versioning** - Extract version from path/headers/query
68    /// 9. **CORS** - Handle preflight & add headers
69    /// 10. **Security headers (Helmet)** - Apply to all responses
70    /// 11. **Logging** - Log all requests
71    /// 12. **Metrics** - Measure all requests
72    /// 13. **Readiness** - Database health check (benefits from timeout/rate limiting)
73    /// 14. **Timeout** - Set timeout boundary for everything (optional)
74    /// 15. **Rate limiting** - Reject excessive requests early
75    /// 16. **Request ID** - Generate/extract ID for tracing (early for observability)
76    /// 17. **Liveness** - Simple health check (always accessible, very early)
77    /// 18. **Panic catching** - Catch ALL panics from inner layers (outermost)
78    ///
79    /// # Manual Setup (Advanced)
80    ///
81    /// If you need custom ordering, call individual `setup_*` methods. **Important rules**:
82    ///
83    /// - **Call order matters**: Methods must be called in reverse execution order
84    ///   (first method called = innermost layer = executes last on request)
85    /// - **Dependencies**: Some middleware depends on others:
86    ///   - `setup_request_id()` must be called **after** `setup_deduplication()` so the
87    ///     request ID is available when deduplication checks for duplicates
88    ///   - `setup_oidc()` requires `setup_session_handling()` (when using sessions)
89    /// - **Don't call twice**: Each `setup_*` method should only be called once
90    /// - **Configuration controls**: Use `[http.middleware] exclude/include` instead of
91    ///   skipping methods, as this ensures proper dependency handling
92    ///
93    /// ```rust,no_run
94    /// # use axum_conf::{Config, FluentRouter, Result};
95    /// # async fn example() -> Result<()> {
96    /// // Manual setup example (not recommended unless you need custom ordering)
97    /// let router = FluentRouter::without_state(Config::default())?
98    ///     // Innermost layers first (execute last on request)
99    ///     .setup_deduplication()
100    ///     .setup_logging()
101    ///     .setup_readiness()   // /ready - after timeout/rate limiting (benefits from protection)
102    ///     .setup_timeout()
103    ///     .setup_rate_limiting()
104    ///     .setup_request_id()  // Outer to deduplication, generates ID early
105    ///     .setup_liveness()    // /live - always accessible, very early
106    ///     .setup_catch_panic();  // Outermost (executes first on request)
107    /// # Ok(())
108    /// # }
109    /// ```
110    ///
111    /// # Returns
112    ///
113    /// A `Result` containing the configured router or an error if setup fails.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if:
118    /// - OIDC configuration is invalid (when `keycloak` feature enabled)
119    /// - Configuration validation fails
120    ///
121    /// # Note
122    ///
123    /// Disable Prometheus in tests to avoid global registry conflicts:
124    /// ```rust
125    /// # use axum_conf::Config;
126    /// let mut config = Config::default();
127    /// config.http.with_metrics = false;
128    /// ```
129    pub async fn setup_middleware(self) -> Result<Self> {
130        // Output the current version of the service
131        const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
132        const VERSION: &str = env!("CARGO_PKG_VERSION");
133        tracing::info!("Starting {PACKAGE_NAME} version {VERSION}...");
134
135        // Capture config values before moving self
136        let default_api_version = self.config.http.default_api_version;
137
138        // Middleware is added from innermost to outermost
139        // The last layer added executes FIRST on incoming requests
140        // Note: route_layer applies to routes added BEFORE it, so OIDC auth is applied first,
141        // then health endpoints are added AFTER (so they're not protected by auth)
142
143        // Protected static files must be added BEFORE auth so route_layer applies to them
144        let router = self.setup_protected_files()?;
145
146        #[cfg(feature = "keycloak")]
147        let router = router.setup_oidc()?; // 1a. OIDC Authentication (route_layer - applies to existing routes)
148
149        #[cfg(feature = "basic-auth")]
150        let router = router.setup_basic_auth()?; // 1b. Basic Auth (route_layer - applies to existing routes)
151
152        // Public static files added AFTER auth so they're accessible without authentication
153        let router = router.setup_public_files()?;
154
155        let router = router.setup_user_span(); // 1c. Record username to span (after auth)
156
157        let router = router
158            .setup_deduplication() // 2. Deduplication
159            .setup_concurrency_limit() // 3. Concurrency control
160            .setup_max_payload_size() // 4. Body size limits
161            .setup_compression() // 5. Compression/decompression
162            .setup_path_normalization() // 6. Path normalization
163            .setup_sensitive_headers() // 7. Filter sensitive headers
164            .setup_api_versioning(default_api_version) // 8. API versioning
165            .setup_cors() // 9. CORS handling
166            .setup_helmet() // 10. Security headers
167            .setup_logging() // 11. Request/response logging
168            .setup_metrics() // 12. Metrics collection
169            .setup_readiness() // 13. Readiness endpoint (benefits from timeout/rate limiting)
170            .setup_timeout() // 14. Request timeout (optional)
171            .setup_rate_limiting() // 15. Rate limiting
172            .setup_request_id() // 16. Request ID - early so all requests get IDs
173            .setup_liveness() // 17. Liveness endpoint (always accessible, very early)
174            .setup_catch_panic() // 18. Outermost - panic recovery
175            .setup_fallback_files()?; // 19. Fallback static files (must be last)
176
177        Ok(router)
178    }
179
180    /// Adds the remaining standard middleware layers in the correct order.
181    /// These layers should be added last as they handle security, errors and panics.
182    /// Since they are added last, they are the outermost layers and thus executed first.
183    ///
184    /// # Deprecated
185    ///
186    /// This method is deprecated. Use `setup_middleware()` instead, which now includes
187    /// all middleware layers in the optimal order. This method is kept for backward
188    /// compatibility but does nothing.
189    #[must_use]
190    #[deprecated(
191        since = "0.2.2",
192        note = "Use setup_middleware() instead, which now includes all layers"
193    )]
194    pub fn build(self) -> Self {
195        // All middleware is now configured in setup_middleware()
196        // This method is a no-op for backward compatibility
197        self
198    }
199
200    /// Starts the HTTP server based on the current configuration.
201    ///
202    /// The server supports both HTTP/1.1 and HTTP/2 protocols automatically.
203    /// HTTP/2 will be used when clients request it via ALPN negotiation.
204    ///
205    /// # Graceful Shutdown
206    ///
207    /// When a shutdown signal is received (SIGTERM or SIGINT), the server:
208    ///
209    /// 1. Emits [`ShutdownPhase::Initiated`] to all subscribers
210    /// 2. Triggers the cancellation token (stopping background tasks)
211    /// 3. Stops accepting new connections
212    /// 4. Emits [`ShutdownPhase::GracePeriodStarted`] with the configured timeout
213    /// 5. Waits for in-flight requests to complete (up to `shutdown_timeout`)
214    /// 6. Emits [`ShutdownPhase::GracePeriodEnded`] if timeout expires
215    /// 7. Exits
216    ///
217    /// If all connections drain before the timeout, shutdown completes early
218    /// without waiting for the full timeout duration.
219    ///
220    /// Components can subscribe to these phases before calling `start()`:
221    ///
222    /// ```rust,no_run
223    /// use axum_conf::{Config, FluentRouter, ShutdownPhase};
224    ///
225    /// # async fn example() -> axum_conf::Result<()> {
226    /// let router = FluentRouter::without_state(Config::default())?;
227    ///
228    /// // Set up shutdown handlers BEFORE starting
229    /// let mut shutdown_rx = router.subscribe_to_shutdown();
230    ///
231    /// tokio::spawn(async move {
232    ///     while let Ok(phase) = shutdown_rx.recv().await {
233    ///         tracing::info!("Shutdown phase: {:?}", phase);
234    ///     }
235    /// });
236    ///
237    /// // Now start the server
238    /// router.setup_middleware().await?.start().await
239    /// # }
240    /// ```
241    pub async fn start(self) -> Result<()>
242    where
243        State: Clone + Send + Sync + 'static,
244    {
245        let bind_addr = self.config.http.full_bind_addr();
246        let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
247
248        tracing::info!("Bound to {}", &bind_addr);
249        tracing::info!("Waiting for connections");
250        tracing::info!("Max req/s: {}", self.config.http.max_requests_per_sec);
251
252        let service = self
253            .inner
254            .with_state(self.state)
255            .into_make_service_with_connect_info::<SocketAddr>();
256
257        let shutdown_timeout = self.config.http.shutdown_timeout;
258        let shutdown_notifier = self.shutdown_notifier.clone();
259
260        // Subscribe to shutdown notifications to know when signal is received
261        let mut shutdown_rx = shutdown_notifier.subscribe();
262
263        let serve_future = axum::serve(listener, service).with_graceful_shutdown(
264            shutdown_signal_with_notifications(shutdown_timeout, shutdown_notifier.clone()),
265        );
266
267        // Wait for graceful shutdown with timeout enforcement.
268        // The timeout only starts AFTER a shutdown signal is received, not immediately.
269        // If connections drain before timeout, we complete early.
270        // If timeout expires first, we emit GracePeriodEnded and force shutdown.
271        tokio::select! {
272            result = serve_future => {
273                // Server shut down gracefully (connections drained)
274                tracing::info!("Graceful shutdown completed");
275                result?;
276            }
277            _ = async {
278                // Wait for shutdown to be initiated before starting the timeout
279                loop {
280                    match shutdown_rx.recv().await {
281                        Ok(ShutdownPhase::Initiated) => break,
282                        Ok(_) => continue,
283                        Err(_) => return, // Channel closed
284                    }
285                }
286                // Now start the timeout (only after signal received)
287                tokio::time::sleep(shutdown_timeout).await;
288            } => {
289                // Timeout expired after shutdown was initiated
290                tracing::warn!("Graceful shutdown timeout expired, forcing shutdown");
291                shutdown_notifier.emit(ShutdownPhase::GracePeriodEnded);
292            }
293        }
294
295        Ok(())
296    }
297
298    /// Adds a custom Tower middleware layer to the router.
299    ///
300    /// This is a low-level method that forwards to `axum::Router::layer()`,
301    /// allowing you to add custom middleware that isn't provided by the library.
302    ///
303    /// # Type Parameters
304    ///
305    /// * `L` - A Tower Layer that produces services compatible with Axum
306    ///
307    /// # Examples
308    ///
309    /// ```rust,no_run
310    /// use tower::limit::ConcurrencyLimitLayer;
311    /// # use axum_conf::{Config, FluentRouter};
312    /// # fn example() -> axum_conf::Result<()> {
313    ///
314    /// let router = FluentRouter::without_state(Config::default())?
315    ///     .layer(ConcurrencyLimitLayer::new(100));
316    /// # Ok(())
317    /// # }
318    /// ```
319    #[must_use]
320    pub fn layer<L>(mut self, layer: L) -> Self
321    where
322        L: Layer<Route> + Clone + Send + Sync + 'static,
323        L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
324        <L::Service as Service<Request<Body>>>::Response: axum::response::IntoResponse + 'static,
325        <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
326        <L::Service as Service<Request<Body>>>::Future: Send + 'static,
327    {
328        self.inner = self.inner.layer(layer);
329        self
330    }
331
332    /// Adds a new route to the router at the specified path.
333    ///
334    /// Routes define how HTTP requests to specific paths are handled.
335    /// Use the routing helpers from `axum::routing` to create method routers:
336    /// - `get()` - Handle GET requests
337    /// - `post()` - Handle POST requests
338    /// - `put()` - Handle PUT requests
339    /// - `delete()` - Handle DELETE requests
340    /// - And more...
341    ///
342    /// # Arguments
343    ///
344    /// * `path` - The URL path pattern for this route (e.g., "/users/:id")
345    /// * `route` - A `MethodRouter` created with `axum::routing` helpers
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use axum_conf::{Config, FluentRouter};
351    /// use axum::routing::get;
352    ///
353    /// async fn handler() -> &'static str {
354    ///     "Hello, World!"
355    /// }
356    ///
357    /// # async fn example() {
358    /// let config = Config::default();
359    /// let router = FluentRouter::without_state(config)
360    ///     .unwrap()
361    ///     .route("/hello", get(handler))
362    ///     .into_inner();
363    /// # }
364    /// ```
365    #[must_use]
366    pub fn route(mut self, path: &str, route: axum::routing::MethodRouter<State>) -> Self {
367        self.inner = self.inner.route(path, route);
368        self
369    }
370
371    /// Adds a middleware layer that only applies to routes, not services.
372    ///
373    /// This is a low-level method that forwards to `axum::Router::route_layer()`.
374    /// Unlike `layer()`, this only affects route handlers and doesn't wrap
375    /// nested services.
376    ///
377    /// # Type Parameters
378    ///
379    /// * `L` - A Tower Layer that produces services compatible with Axum
380    ///
381    /// # Use Cases
382    ///
383    /// Use this when you want middleware to only affect your route handlers
384    /// but not services like `ServeDir` or nested routers.
385    #[must_use]
386    pub fn route_layer<L>(mut self, layer: L) -> Self
387    where
388        L: Layer<Route> + Clone + Send + Sync + 'static,
389        L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
390        <L::Service as Service<Request<Body>>>::Response: axum::response::IntoResponse + 'static,
391        <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
392        <L::Service as Service<Request<Body>>>::Future: Send + 'static,
393    {
394        self.inner = self.inner.route_layer(layer);
395        self
396    }
397
398    /// Nests another router at a specific path prefix.
399    ///
400    /// All routes in the nested router will be prefixed with the given path.
401    /// Middleware added to the nested router only affects its own routes.
402    ///
403    /// # Arguments
404    ///
405    /// * `path` - The path prefix (must start with `/`)
406    /// * `router` - The router to nest
407    ///
408    /// # Examples
409    ///
410    /// ```rust,no_run
411    /// use axum::{Router, routing::get};
412    /// # use axum_conf::{Config, FluentRouter};
413    /// # fn example() -> axum_conf::Result<()> {
414    ///
415    /// let api_v1 = Router::new()
416    ///     .route("/users", get(|| async { "users" }));
417    ///
418    /// let app = FluentRouter::without_state(Config::default())?
419    ///     .nest("/api/v1", api_v1);  // Routes at /api/v1/users
420    /// # Ok(())
421    /// # }
422    /// ```
423    #[must_use]
424    pub fn nest(mut self, path: &str, router: Router<State>) -> Self {
425        self.inner = self.inner.nest(path, router);
426        self
427    }
428
429    /// Nests a Tower service at a specific path prefix.
430    ///
431    /// Similar to `nest()` but for raw Tower services instead of Axum routers.
432    /// Commonly used for serving static files with `ServeDir`.
433    ///
434    /// # Arguments
435    ///
436    /// * `path` - The path prefix (must start with `/`)
437    /// * `service` - The Tower service to nest
438    ///
439    /// # Examples
440    ///
441    /// ```rust,no_run
442    /// use axum::{Router, routing::get};
443    /// # use axum_conf::{Config, FluentRouter};
444    /// # fn example() -> axum_conf::Result<()> {
445    ///
446    /// let service = Router::new().route("/health", get(|| async { "OK" }));
447    /// let app = FluentRouter::without_state(Config::default())?
448    ///     .nest_service("/api", service);
449    /// # Ok(())
450    /// # }
451    /// ```
452    #[must_use]
453    pub fn nest_service<T>(mut self, path: &str, service: T) -> Self
454    where
455        T: Service<Request<Body>, Response = axum::response::Response, Error = Infallible>
456            + Clone
457            + Send
458            + Sync
459            + 'static,
460        T::Future: Send + 'static,
461    {
462        self.inner = self.inner.nest_service(path, service);
463        self
464    }
465
466    /// Merges another router into this one.
467    ///
468    /// Routes and services from the other router are added to this router.
469    /// Unlike `nest()`, routes are not prefixed - they're added at the same level.
470    ///
471    /// # Arguments
472    ///
473    /// * `other` - The router to merge
474    ///
475    /// # Examples
476    ///
477    /// ```rust,no_run
478    /// use axum::{Router, routing::get};
479    /// # use axum_conf::{Config, FluentRouter};
480    /// # fn example() -> axum_conf::Result<()> {
481    ///
482    /// let user_routes = Router::new()
483    ///     .route("/users", get(|| async { "users" }));
484    ///
485    /// let app = FluentRouter::without_state(Config::default())?
486    ///     .merge(user_routes);  // Routes directly at /users
487    /// # Ok(())
488    /// # }
489    /// ```
490    ///
491    /// # Common Pattern
492    ///
493    /// Use `merge()` to combine route modules:
494    /// ```rust,no_run
495    /// # use axum::Router;
496    /// # use axum_conf::{Config, FluentRouter};
497    /// # fn example() -> axum_conf::Result<()> {
498    /// # fn api_routes() -> Router { Router::new() }
499    /// # fn admin_routes() -> Router { Router::new() }
500    /// FluentRouter::without_state(Config::default())?
501    ///     .merge(api_routes())
502    ///     .merge(admin_routes());
503    /// # Ok(())
504    /// # }
505    /// ```
506    #[must_use]
507    pub fn merge(mut self, other: Router<State>) -> Self {
508        self.inner = self.inner.merge(other);
509        self
510    }
511
512    /// Adds a Tower service at a specific route.
513    ///
514    /// Unlike `nest_service()`, this adds the service at an exact path rather
515    /// than a path prefix.
516    ///
517    /// # Arguments
518    ///
519    /// * `path` - The exact route path
520    /// * `service` - The Tower service to add
521    ///
522    /// # Examples
523    ///
524    /// ```rust,no_run
525    /// use tower::service_fn;
526    /// use http::Response;
527    /// # use axum_conf::{Config, FluentRouter};
528    /// # async fn example() -> axum_conf::Result<()> {
529    ///
530    /// let service = service_fn(|_req| async {
531    ///     Ok::<_, std::convert::Infallible>(Response::new("Hello".into()))
532    /// });
533    ///
534    /// let app = FluentRouter::without_state(Config::default())?
535    ///     .route_service("/custom", service);
536    /// # Ok(())
537    /// # }
538    /// ```
539    #[must_use]
540    pub fn route_service<T>(mut self, path: &str, service: T) -> Self
541    where
542        T: Service<Request<Body>, Response = axum::response::Response, Error = Infallible>
543            + Clone
544            + Send
545            + Sync
546            + 'static,
547        T::Future: Send + 'static,
548    {
549        self.inner = self.inner.route_service(path, service);
550        self
551    }
552
553    /// Consumes the `FluentRouter` and returns the underlying `axum::Router`.
554    ///
555    /// Use this when you need direct access to the Axum router, typically for
556    /// testing or when you want to add additional middleware that requires
557    /// the concrete `Router` type.
558    ///
559    /// # Examples
560    ///
561    /// ```rust,no_run
562    /// # use axum_conf::{Config, FluentRouter};
563    /// # fn example() -> axum_conf::Result<()> {
564    /// let fluent = FluentRouter::without_state(Config::default())?;
565    /// let axum_router: axum::Router = fluent.into_inner();
566    /// # Ok(())
567    /// # }
568    /// ```
569    pub fn into_inner(self) -> Router<State> {
570        self.inner
571    }
572}
573
574/// Returns a signal handler that emits shutdown phase notifications.
575///
576/// This function:
577/// 1. Waits for SIGTERM or SIGINT (Ctrl+C)
578/// 2. Emits [`ShutdownPhase::Initiated`] (and triggers the cancellation token)
579/// 3. Emits [`ShutdownPhase::GracePeriodStarted`] with the configured timeout
580/// 4. Returns immediately to let axum start graceful shutdown
581///
582/// The grace period timeout is enforced by the caller (see [`FluentRouter::start`]),
583/// which wraps the serve call with a timeout. When connections drain before the
584/// timeout, shutdown completes early. If the timeout expires first,
585/// [`ShutdownPhase::GracePeriodEnded`] is emitted and shutdown is forced.
586///
587/// Components can subscribe to these phases to perform coordinated cleanup.
588///
589/// If signal registration fails, the function logs a warning and falls back to
590/// waiting indefinitely. This ensures the server continues running even if signal
591/// handlers cannot be installed (e.g., in restricted environments).
592pub(crate) async fn shutdown_signal_with_notifications(
593    timeout: Duration,
594    notifier: ShutdownNotifier,
595) {
596    let ctrl_c = async {
597        match signal::ctrl_c().await {
598            Ok(()) => {
599                tracing::debug!("Ctrl+C signal received");
600            }
601            Err(err) => {
602                tracing::warn!("Failed to install Ctrl+C handler: {}", err);
603                // Wait indefinitely if we can't install the handler
604                std::future::pending::<()>().await;
605            }
606        }
607    };
608
609    #[cfg(unix)]
610    let terminate = async {
611        match signal::unix::signal(signal::unix::SignalKind::terminate()) {
612            Ok(mut signal_handler) => {
613                signal_handler.recv().await;
614                tracing::debug!("SIGTERM signal received");
615            }
616            Err(err) => {
617                tracing::warn!("Failed to install SIGTERM handler: {}", err);
618                // Wait indefinitely if we can't install the handler
619                std::future::pending::<()>().await;
620            }
621        }
622    };
623
624    #[cfg(not(unix))]
625    let terminate = std::future::pending::<()>();
626
627    tokio::select! {
628        _ = ctrl_c => {},
629        _ = terminate => {},
630    }
631
632    // Phase 1: Initiated - signal received, cancellation token triggered
633    tracing::info!(
634        "Shutdown signal received, starting graceful shutdown (timeout: {}s)",
635        timeout.as_secs()
636    );
637    let subscriber_count = notifier.emit(ShutdownPhase::Initiated);
638    tracing::debug!(
639        "Shutdown initiated notification sent to {} subscriber(s)",
640        subscriber_count
641    );
642
643    // Phase 2: Grace period started - in-flight requests draining
644    // Return immediately to let axum start graceful shutdown.
645    // The timeout is enforced by the caller wrapping the serve call.
646    notifier.emit(ShutdownPhase::GracePeriodStarted { timeout });
647}
648
649/// Returns a signal handler that allows us to stop the server using Ctrl+C
650/// or the terminate signal, which in turn allows us to perform a graceful
651/// shutdown with a configurable timeout.
652///
653/// If signal registration fails, the function logs a warning and falls back to
654/// waiting indefinitely. This ensures the server continues running even if signal
655/// handlers cannot be installed (e.g., in restricted environments).
656///
657/// # Deprecated
658///
659/// This function is deprecated. Use [`shutdown_signal_with_notifications`] instead,
660/// which emits [`ShutdownPhase`] events for coordinated shutdown handling.
661/// Note: The timeout is now enforced by the caller, not within this function.
662#[allow(dead_code)]
663#[deprecated(
664    since = "0.4.0",
665    note = "Use shutdown_signal_with_notifications instead for shutdown phase notifications"
666)]
667pub(crate) async fn shutdown_signal_with_timeout(timeout: Duration) {
668    shutdown_signal_with_notifications(timeout, ShutdownNotifier::default()).await;
669}