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
53impl std::fmt::Display for ApiFailure {
54    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(formatter, "{}: {}", self.status, self.message)
56    }
57}
58
59impl From<ApiError> for ApiFailure {
60    fn from(error: ApiError) -> Self {
61        let mut failure = Self::new(error.status, error.message).with_code(error.code);
62        failure.busy = error.busy;
63        failure
64    }
65}
66
67impl From<anyhow::Error> for ApiFailure {
68    fn from(error: anyhow::Error) -> Self {
69        if let Some(refusal) = mj_core::refusal::Refusal::of(&error) {
70            return match refusal.kind() {
71                mj_core::refusal::RefusalKind::Precondition => Self::conflict(refusal.message()),
72                mj_core::refusal::RefusalKind::Unusable => Self::bad_request(refusal.message()),
73            }
74            .with_code(refusal.code());
75        }
76        Self::new(StatusCode::INTERNAL_SERVER_ERROR, format!("{error:#}"))
77    }
78}
79
80#[derive(Debug, Serialize)]
81pub(super) struct FailureBody {
82    pub(super) error: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub(super) code: Option<&'static str>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub(super) running_actions: Option<usize>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub(super) action_limit: Option<usize>,
89}
90
91impl IntoResponse for ApiFailure {
92    fn into_response(self) -> Response {
93        (
94            self.status,
95            Json(FailureBody {
96                error: self.message,
97                code: self.code,
98                running_actions: self.busy.map(|(running, _)| running),
99                action_limit: self.busy.map(|(_, limit)| limit),
100            }),
101        )
102            .into_response()
103    }
104}
105
106// ---------------------------------------------------------------------------
107// Wire types
108// ---------------------------------------------------------------------------