mj_controller/server/api/
failure.rs1use super::*;
2
3#[derive(Debug)]
10pub struct ApiFailure {
11 pub status: StatusCode,
12 pub message: String,
13 pub code: Option<&'static str>,
15 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 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 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
70pub 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 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