Skip to main content

mj_controller/server/api/
failure.rs

1use super::*;
2
3/// An API failure with a message written for the caller.
4///
5/// The phone surface deliberately answers with fixed strings, because its
6/// errors would otherwise name profile homes and SSH hosts to a browser. Here
7/// the caller is the same user who owns the daemon, and the whole value of the
8/// API is knowing *why* a turn or an export failed, so the message is dynamic.
9#[derive(Debug)]
10pub struct ApiFailure {
11    pub status: StatusCode,
12    pub message: String,
13    /// Names a refusal's reason for a client that chooses its own remedy.
14    pub code: Option<&'static str>,
15    /// `(running, limit)` when the refusal is a full action pool; see
16    /// [`ApiError::with_busy`], the one place that sets it.
17    busy: Option<(usize, usize)>,
18}
19
20impl ApiFailure {
21    pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
22        Self {
23            status,
24            message: message.into(),
25            code: None,
26            busy: None,
27        }
28    }
29
30    #[must_use]
31    pub fn with_code(mut self, code: Option<&'static str>) -> Self {
32        self.code = code;
33        self
34    }
35
36    pub fn bad_request(message: impl Into<String>) -> Self {
37        Self::new(StatusCode::BAD_REQUEST, message)
38    }
39
40    pub fn conflict(message: impl Into<String>) -> Self {
41        Self::new(StatusCode::CONFLICT, message)
42    }
43
44    pub fn not_found(message: impl Into<String>) -> Self {
45        Self::new(StatusCode::NOT_FOUND, message)
46    }
47
48    pub fn unavailable(message: impl Into<String>) -> Self {
49        Self::new(StatusCode::SERVICE_UNAVAILABLE, message)
50    }
51
52    /// The answer to a long request, such as a wait or an event stream, that
53    /// an automatic upgrade handoff ended. Its client asks the next daemon.
54    pub fn handoff() -> Self {
55        Self::unavailable("the Mjolnir daemon is being replaced by an upgrade; ask again")
56            .with_code(Some(DAEMON_HANDOFF_CODE))
57    }
58
59    /// The answer to a long request that a shutdown ended: a handoff when the
60    /// daemon is being replaced, otherwise a plain refusal.
61    pub(super) fn shutdown(state: &ServerState) -> Self {
62        if state.handing_off() {
63            Self::handoff()
64        } else {
65            Self::unavailable("the server is shutting down")
66        }
67    }
68}
69
70/// The failure code of [`ApiFailure::handoff`].
71pub const DAEMON_HANDOFF_CODE: &str = "daemon_handoff";
72
73impl std::fmt::Display for ApiFailure {
74    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(formatter, "{}: {}", self.status, self.message)
76    }
77}
78
79impl From<ApiError> for ApiFailure {
80    fn from(error: ApiError) -> Self {
81        let mut failure = Self::new(error.status, error.message).with_code(error.code);
82        failure.busy = error.busy;
83        failure
84    }
85}
86
87impl From<anyhow::Error> for ApiFailure {
88    fn from(error: anyhow::Error) -> Self {
89        if let Some(refusal) = mj_core::refusal::Refusal::of(&error) {
90            return match refusal.kind() {
91                mj_core::refusal::RefusalKind::Precondition => Self::conflict(refusal.message()),
92                mj_core::refusal::RefusalKind::Unusable => Self::bad_request(refusal.message()),
93            }
94            .with_code(refusal.code());
95        }
96        Self::new(StatusCode::INTERNAL_SERVER_ERROR, format!("{error:#}"))
97    }
98}
99
100#[derive(Debug, Serialize)]
101pub(super) struct FailureBody {
102    pub(super) error: String,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub(super) code: Option<&'static str>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub(super) running_actions: Option<usize>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub(super) action_limit: Option<usize>,
109}
110
111impl IntoResponse for ApiFailure {
112    fn into_response(self) -> Response {
113        let handoff = self.code == Some(DAEMON_HANDOFF_CODE);
114        let mut response = (
115            self.status,
116            Json(FailureBody {
117                error: self.message,
118                code: self.code,
119                running_actions: self.busy.map(|(running, _)| running),
120                action_limit: self.busy.map(|(_, limit)| limit),
121            }),
122        )
123            .into_response();
124        if handoff {
125            // The same marks the admission layer puts on a refused request.
126            let headers = response.headers_mut();
127            headers.insert("retry-after", axum::http::HeaderValue::from_static("1"));
128            headers.insert(
129                crate::server::UPGRADE_HEADER,
130                axum::http::HeaderValue::from_static("pending"),
131            );
132        }
133        response
134    }
135}
136
137// ---------------------------------------------------------------------------
138// Wire types
139// ---------------------------------------------------------------------------