Axum middleware that gates non-health routes on [`StartupPhase::GatewayEnable`]. All `/health*` paths (liveness, readiness, drain) are always let through so k8s probes can observe startup progress. All other routes receive a `503 Service Unavailable` until the node reaches `GatewayEnable`.
(
State(app_state): State<AppState>,
req: axum::http::Request<axum::body::Body>,
next: Next,
)
| 176 | /// k8s probes can observe startup progress. All other routes receive a |
| 177 | /// `503 Service Unavailable` until the node reaches `GatewayEnable`. |
| 178 | async fn startup_gate_middleware( |
| 179 | State(app_state): State<AppState>, |
| 180 | req: axum::http::Request<axum::body::Body>, |
| 181 | next: Next, |
| 182 | ) -> Response { |
| 183 | use axum::http::StatusCode; |
| 184 | use axum::response::IntoResponse; |
| 185 | |
| 186 | let path = req.uri().path(); |
| 187 | // Health-probe paths bypass the gate — these must be reachable during startup. |
| 188 | let is_health_path = path == "/healthz" || path.starts_with("/health/"); |
| 189 | |
| 190 | if !is_health_path { |
| 191 | let gate = &app_state.shared.startup; |
| 192 | let snap = gate.current_phase(); |
| 193 | if let Some(err) = gate.is_failed() { |
| 194 | let body = serde_json::json!({ |
| 195 | "status": "failed", |
| 196 | "error": err.to_string(), |
| 197 | }); |
| 198 | return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(body)).into_response(); |
| 199 | } |
| 200 | if snap < crate::control::startup::StartupPhase::GatewayEnable { |
| 201 | let body = serde_json::json!({ |
| 202 | "status": "starting", |
| 203 | "phase": snap.name(), |
| 204 | }); |
| 205 | return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(body)).into_response(); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | next.run(req).await |
| 210 | } |
| 211 | |
| 212 | /// Start the HTTP API server from an already-bound [`tokio::net::TcpListener`]. |
| 213 | /// |
nothing calls this directly
no test coverage detected