Skip to main content

soma_http_server/
health.rs

1//! Generic health-check route wiring.
2//!
3//! `soma_http_api::probe` owns the liveness/readiness response *bodies*.
4//! This module owns mounting them as ready-to-merge routers so a product
5//! doesn't have to hand-roll the same two routes itself. Both are generic
6//! over the router's state type so a product can `.merge()` them into a
7//! bigger stateful router without a state-shape mismatch.
8//!
9//! Not yet adopted: `apps/soma`'s `/health` and `/readyz` routes are still
10//! hand-implemented in `crates/soma/api` rather than mounting these
11//! routers. This module is available scaffolding for that future
12//! consolidation, not something already wired into a live Soma surface.
13
14use std::future::Future;
15
16use axum::{routing::get, Router};
17use soma_http_api::probe::{liveness_response, readiness_response};
18
19/// Mount `GET /health`, an always-`200 OK` liveness probe.
20pub fn liveness_router<S>() -> Router<S>
21where
22    S: Clone + Send + Sync + 'static,
23{
24    Router::new().route("/health", get(|| async { liveness_response() }))
25}
26
27/// Mount `GET /readyz`, a readiness probe backed by `check`.
28///
29/// `check` runs on every request. `Ok(())` renders `200 OK`; `Err(e)`
30/// renders `503 Service Unavailable` with `e.to_string()` as the reason —
31/// see [`soma_http_api::probe::readiness_response`].
32pub fn readiness_router<S, F, Fut, E>(check: F) -> Router<S>
33where
34    S: Clone + Send + Sync + 'static,
35    F: Fn() -> Fut + Clone + Send + Sync + 'static,
36    Fut: Future<Output = Result<(), E>> + Send,
37    E: std::fmt::Display,
38{
39    Router::new().route(
40        "/readyz",
41        get(move || {
42            let check = check.clone();
43            async move { readiness_response(check().await) }
44        }),
45    )
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use axum::{body::to_bytes, body::Body, http::Request, http::StatusCode};
52    use tower::ServiceExt;
53
54    #[tokio::test]
55    async fn liveness_router_reports_ok() {
56        let app: Router<()> = liveness_router();
57        let request = Request::builder()
58            .uri("/health")
59            .body(Body::empty())
60            .unwrap();
61        let response = app.oneshot(request).await.unwrap();
62        assert_eq!(response.status(), StatusCode::OK);
63        let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
64        let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
65        assert_eq!(value["status"], "ok");
66    }
67
68    #[tokio::test]
69    async fn readiness_router_reports_ready_when_check_succeeds() {
70        let app: Router<()> = readiness_router(|| async { Ok::<(), &'static str>(()) });
71        let request = Request::builder()
72            .uri("/readyz")
73            .body(Body::empty())
74            .unwrap();
75        let response = app.oneshot(request).await.unwrap();
76        assert_eq!(response.status(), StatusCode::OK);
77    }
78
79    #[tokio::test]
80    async fn readiness_router_reports_unavailable_when_check_fails() {
81        let app: Router<()> =
82            readiness_router(|| async { Err::<(), &'static str>("upstream down") });
83        let request = Request::builder()
84            .uri("/readyz")
85            .body(Body::empty())
86            .unwrap();
87        let response = app.oneshot(request).await.unwrap();
88        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
89        let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
90        let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
91        assert_eq!(value["reason"], "upstream down");
92    }
93}