Skip to main content

soma_http_api/
probe.rs

1//! Reusable liveness/readiness probe DTOs and response helpers.
2//!
3//! Products wire these into their own `/health` and `/readyz` handlers,
4//! supplying whatever async check represents "ready" for them.
5
6use axum::{
7    http::StatusCode,
8    response::{IntoResponse, Response},
9    Json,
10};
11use serde::Serialize;
12
13/// The one liveness value this crate emits. An enum (rather than a bare
14/// `&'static str`) so a second construction site can't drift to an
15/// unintended value — mirrors `ReadinessStatus` below.
16#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18pub enum LivenessStatus {
19    Ok,
20}
21
22#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
23pub struct LivenessBody {
24    pub status: LivenessStatus,
25}
26
27impl Default for LivenessBody {
28    fn default() -> Self {
29        Self {
30            status: LivenessStatus::Ok,
31        }
32    }
33}
34
35/// The two readiness values this crate emits. An enum (rather than a bare
36/// `&'static str`) so callers can't construct a third, unintended status
37/// value that this module's response builders never produce.
38#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum ReadinessStatus {
41    Ready,
42    NotReady,
43}
44
45#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
46pub struct ReadinessBody {
47    pub status: ReadinessStatus,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub reason: Option<String>,
50}
51
52/// `200 OK` liveness response — "the process is up".
53pub fn liveness_response() -> Response {
54    Json(LivenessBody::default()).into_response()
55}
56
57/// Readiness response from a dependency check: `200 OK` with `{"status":
58/// "ready"}` on success, `503 Service Unavailable` with the failure reason
59/// on error. Unlike liveness, readiness probes an upstream dependency so
60/// orchestrators only route traffic once the service can actually serve it.
61pub fn readiness_response<E: std::fmt::Display>(result: Result<(), E>) -> Response {
62    match result {
63        Ok(()) => (
64            StatusCode::OK,
65            Json(ReadinessBody {
66                status: ReadinessStatus::Ready,
67                reason: None,
68            }),
69        )
70            .into_response(),
71        Err(error) => (
72            StatusCode::SERVICE_UNAVAILABLE,
73            Json(ReadinessBody {
74                status: ReadinessStatus::NotReady,
75                reason: Some(error.to_string()),
76            }),
77        )
78            .into_response(),
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use axum::body::to_bytes;
85
86    use super::*;
87
88    #[tokio::test]
89    async fn liveness_response_reports_ok() {
90        let response = liveness_response();
91        assert_eq!(response.status(), StatusCode::OK);
92        let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
93        let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
94        assert_eq!(value["status"], "ok");
95    }
96
97    #[tokio::test]
98    async fn readiness_response_ok_reports_ready() {
99        let response = readiness_response::<std::convert::Infallible>(Ok(()));
100        assert_eq!(response.status(), StatusCode::OK);
101        let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
102        let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
103        assert_eq!(value["status"], "ready");
104        assert!(value.get("reason").is_none());
105    }
106
107    #[tokio::test]
108    async fn readiness_response_err_reports_reason_and_503() {
109        let response = readiness_response(Err("upstream unreachable"));
110        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
111        let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
112        let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
113        assert_eq!(value["status"], "not_ready");
114        assert_eq!(value["reason"], "upstream unreachable");
115    }
116}