error.rs
⎇
Raw
1use std::path::PathBuf;
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5
6use crate::db::Db;
7
8#[derive(Clone)]
9pub struct AppState {
10 pub db: Db,
11 /// Canonicalized server root folder.
12 pub root: PathBuf,
13 /// Whether we sit behind a TLS-terminating reverse proxy.
14 pub https: bool,
15}
16
17/// API error. `extra` is optionally merged into the JSON body (e.g. a list of
18/// conflicting file names on a 409). Rendered by `IntoResponse` below; the
19/// error string itself is never displayed, so no `Display` impl is needed.
20pub struct ApiError(pub StatusCode, pub String, pub Option<serde_json::Value>);
21
22impl ApiError {
23 pub fn new(status: StatusCode, msg: impl Into<String>) -> Self {
24 Self(status, msg.into(), None)
25 }
26
27 pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
28 self.2 = Some(extra);
29 self
30 }
31}
32
33impl IntoResponse for ApiError {
34 fn into_response(self) -> Response {
35 let mut body = serde_json::json!({ "error": self.1 });
36 if let Some(extra) = self.2
37 && let (Some(obj), Some(extra)) = (body.as_object_mut(), extra.as_object())
38 {
39 for (k, v) in extra {
40 obj.insert(k.clone(), v.clone());
41 }
42 }
43 (self.0, axum::Json(body)).into_response()
44 }
45}
46
47impl From<std::io::Error> for ApiError {
48 fn from(e: std::io::Error) -> Self {
49 tracing::error!(error = %e, "io error");
50 ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error")
51 }
52}
53
54impl From<rusqlite::Error> for ApiError {
55 fn from(e: rusqlite::Error) -> Self {
56 tracing::error!(error = %e, "database error");
57 ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error")
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use axum::http::StatusCode;
65 use http_body_util::BodyExt;
66
67 fn status_and_body(e: ApiError) -> (StatusCode, String) {
68 let resp = e.into_response();
69 let status = resp.status();
70 let rt = tokio::runtime::Builder::new_current_thread()
71 .enable_all()
72 .build()
73 .unwrap();
74 let bytes = rt
75 .block_on(async { resp.into_body().collect().await.unwrap().to_bytes() })
76 .to_vec();
77 (status, String::from_utf8(bytes).unwrap())
78 }
79
80 #[test]
81 fn plain_error_body() {
82 let (status, body) =
83 status_and_body(ApiError::new(StatusCode::NOT_FOUND, "folder not found"));
84 assert_eq!(status, StatusCode::NOT_FOUND);
85 assert_eq!(
86 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
87 serde_json::json!({ "error": "folder not found" })
88 );
89 }
90
91 #[test]
92 fn extra_object_is_merged_into_body() {
93 let e = ApiError::new(StatusCode::CONFLICT, "some files already exist")
94 .with_extra(serde_json::json!({ "skipped": ["a.txt"], "uploaded": 2 }));
95 let (status, body) = status_and_body(e);
96 assert_eq!(status, StatusCode::CONFLICT);
97 assert_eq!(
98 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
99 serde_json::json!({
100 "error": "some files already exist",
101 "skipped": ["a.txt"],
102 "uploaded": 2
103 })
104 );
105 }
106
107 #[test]
108 fn non_object_extra_is_ignored() {
109 let e = ApiError::new(StatusCode::CONFLICT, "conflict")
110 .with_extra(serde_json::json!(["not", "an", "object"]));
111 let (_status, body) = status_and_body(e);
112 assert_eq!(
113 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
114 serde_json::json!({ "error": "conflict" })
115 );
116 }
117
118 #[test]
119 fn io_error_maps_to_500() {
120 let e = ApiError::from(std::io::Error::new(
121 std::io::ErrorKind::NotFound,
122 "disk vanished",
123 ));
124 let (status, body) = status_and_body(e);
125 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
126 assert!(body.contains("internal error"));
127 }
128}
129