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).
19#[derive(Debug, thiserror::Error)]
20#[error("{1}")]
21pub struct ApiError(pub StatusCode, pub String, pub Option<serde_json::Value>);
22
23impl ApiError {
24 pub fn new(status: StatusCode, msg: impl Into<String>) -> Self {
25 Self(status, msg.into(), None)
26 }
27
28 pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
29 self.2 = Some(extra);
30 self
31 }
32}
33
34impl IntoResponse for ApiError {
35 fn into_response(self) -> Response {
36 let mut body = serde_json::json!({ "error": self.1 });
37 if let Some(extra) = self.2 {
38 if let (Some(obj), Some(extra)) = (body.as_object_mut(), extra.as_object()) {
39 for (k, v) in extra {
40 obj.insert(k.clone(), v.clone());
41 }
42 }
43 }
44 (self.0, axum::Json(body)).into_response()
45 }
46}
47
48impl From<std::io::Error> for ApiError {
49 fn from(e: std::io::Error) -> Self {
50 tracing::error!(error = %e, "io error");
51 ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error")
52 }
53}
54
55impl From<rusqlite::Error> for ApiError {
56 fn from(e: rusqlite::Error) -> Self {
57 tracing::error!(error = %e, "database error");
58 ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error")
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use axum::http::StatusCode;
66 use http_body_util::BodyExt;
67
68 fn status_and_body(e: ApiError) -> (StatusCode, String) {
69 let resp = e.into_response();
70 let status = resp.status();
71 let rt = tokio::runtime::Builder::new_current_thread()
72 .enable_all()
73 .build()
74 .unwrap();
75 let bytes = rt
76 .block_on(async { resp.into_body().collect().await.unwrap().to_bytes() })
77 .to_vec();
78 (status, String::from_utf8(bytes).unwrap())
79 }
80
81 #[test]
82 fn plain_error_body() {
83 let (status, body) = status_and_body(ApiError::new(
84 StatusCode::NOT_FOUND,
85 "folder not found",
86 ));
87 assert_eq!(status, StatusCode::NOT_FOUND);
88 assert_eq!(
89 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
90 serde_json::json!({ "error": "folder not found" })
91 );
92 }
93
94 #[test]
95 fn extra_object_is_merged_into_body() {
96 let e = ApiError::new(StatusCode::CONFLICT, "some files already exist")
97 .with_extra(serde_json::json!({ "skipped": ["a.txt"], "uploaded": 2 }));
98 let (status, body) = status_and_body(e);
99 assert_eq!(status, StatusCode::CONFLICT);
100 assert_eq!(
101 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
102 serde_json::json!({
103 "error": "some files already exist",
104 "skipped": ["a.txt"],
105 "uploaded": 2
106 })
107 );
108 }
109
110 #[test]
111 fn non_object_extra_is_ignored() {
112 let e = ApiError::new(StatusCode::CONFLICT, "conflict")
113 .with_extra(serde_json::json!(["not", "an", "object"]));
114 let (_status, body) = status_and_body(e);
115 assert_eq!(
116 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
117 serde_json::json!({ "error": "conflict" })
118 );
119 }
120
121 #[test]
122 fn io_error_maps_to_500() {
123 let e = ApiError::from(std::io::Error::new(
124 std::io::ErrorKind::NotFound,
125 "disk vanished",
126 ));
127 let (status, body) = status_and_body(e);
128 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
129 assert!(body.contains("internal error"));
130 }
131}
132