rust_web_server/maintenance/mod.rs
1//! Maintenance mode middleware.
2//!
3//! Set `MAINTENANCE_MODE` to `true` at runtime to return `503 Service
4//! Unavailable` for all requests except `/healthz` and `/readyz`. Clear it to
5//! resume normal traffic.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use rust_web_server::app::App;
11//! use rust_web_server::core::New;
12//! use rust_web_server::maintenance::{MAINTENANCE_MODE, MaintenanceLayer};
13//! use std::sync::atomic::Ordering;
14//!
15//! let app = App::new().wrap(MaintenanceLayer);
16//!
17//! // Enable maintenance mode at runtime.
18//! MAINTENANCE_MODE.store(true, Ordering::SeqCst);
19//! ```
20
21#[cfg(test)]
22mod tests;
23
24use std::sync::atomic::{AtomicBool, Ordering};
25
26use crate::application::Application;
27use crate::middleware::Middleware;
28use crate::request::Request;
29use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
30use crate::server::ConnectionInfo;
31
32/// Flip to `true` to activate maintenance mode; back to `false` to resume.
33pub static MAINTENANCE_MODE: AtomicBool = AtomicBool::new(false);
34
35fn service_unavailable() -> Response {
36 crate::error::problem::problem_with_text(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "maintenance", "Service Temporarily Unavailable")
37}
38
39/// Middleware that returns `503` when [`MAINTENANCE_MODE`] is `true`.
40///
41/// Health probe paths (`/healthz`, `/readyz`) always pass through so that
42/// load balancers can still detect the pod is alive.
43pub struct MaintenanceLayer;
44
45impl Middleware for MaintenanceLayer {
46 /// Headers only: a streamed body passes through untouched.
47 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
48 true
49 }
50
51
52 fn handle(
53 &self,
54 request: &Request,
55 connection: &ConnectionInfo,
56 next: &dyn Application,
57 ) -> Result<Response, String> {
58 if MAINTENANCE_MODE.load(Ordering::Relaxed) {
59 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
60 if path != "/healthz" && path != "/readyz" {
61 return Ok(service_unavailable());
62 }
63 }
64 next.execute(request, connection)
65 }
66
67 /// In-memory only: safe on the event loop.
68 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
69 true
70 }
71
72 /// Off, it does nothing: config-driven proxy routes keep their
73 /// non-blocking forwarder (since 18.10.0, when the binary added this
74 /// layer to proxy mode for the admin API's maintenance switch).
75 fn is_passthrough(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
76 !MAINTENANCE_MODE.load(Ordering::Relaxed)
77 }
78}