unitycatalog_storage_proxy/
error.rs1use axum::http::{StatusCode, header};
15use axum::response::{IntoResponse, Response};
16
17pub type ProxyResult<T> = Result<T, ProxyError>;
19
20#[derive(Debug, thiserror::Error)]
26pub enum ProxyError {
27 #[error("not found: {0}")]
29 NotFound(String),
30
31 #[error("unauthenticated: {0}")]
33 Unauthenticated(String),
34
35 #[error("permission denied: {0}")]
37 PermissionDenied(String),
38
39 #[error("out of scope: {0}")]
42 OutOfScope(String),
43
44 #[error("invalid request: {0}")]
46 InvalidArgument(String),
47
48 #[error("precondition failed: {0}")]
50 PreconditionFailed(String),
51
52 #[error("range not satisfiable: {0}")]
54 RangeNotSatisfiable(String),
55
56 #[error("payload too large: {0}")]
58 PayloadTooLarge(String),
59
60 #[error("not implemented: {0}")]
63 NotImplemented(&'static str),
64
65 #[error(transparent)]
68 Storage(#[from] object_store::Error),
69
70 #[error("internal error: {0}")]
72 Internal(String),
73}
74
75impl ProxyError {
76 fn storage_parts(err: &object_store::Error) -> StatusCode {
80 match err {
81 object_store::Error::NotFound { .. } => StatusCode::NOT_FOUND,
82 object_store::Error::Precondition { .. } => StatusCode::PRECONDITION_FAILED,
83 object_store::Error::NotModified { .. } => StatusCode::NOT_MODIFIED,
84 object_store::Error::AlreadyExists { .. } => StatusCode::CONFLICT,
85 object_store::Error::PermissionDenied { .. } => StatusCode::FORBIDDEN,
86 object_store::Error::Unauthenticated { .. } => StatusCode::UNAUTHORIZED,
87 object_store::Error::NotSupported { .. }
88 | object_store::Error::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED,
89 object_store::Error::InvalidPath { .. } => StatusCode::BAD_REQUEST,
90 _ => StatusCode::INTERNAL_SERVER_ERROR,
91 }
92 }
93
94 fn status(&self) -> StatusCode {
96 match self {
97 ProxyError::NotFound(_) => StatusCode::NOT_FOUND,
98 ProxyError::Unauthenticated(_) => StatusCode::UNAUTHORIZED,
99 ProxyError::PermissionDenied(_) | ProxyError::OutOfScope(_) => StatusCode::FORBIDDEN,
100 ProxyError::InvalidArgument(_) => StatusCode::BAD_REQUEST,
101 ProxyError::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
102 ProxyError::RangeNotSatisfiable(_) => StatusCode::RANGE_NOT_SATISFIABLE,
103 ProxyError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
104 ProxyError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
105 ProxyError::Storage(e) => Self::storage_parts(e),
106 ProxyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
107 }
108 }
109}
110
111impl IntoResponse for ProxyError {
112 fn into_response(self) -> Response {
113 let status = self.status();
114 let message = self.to_string();
115 let body = format!("{{\"error\":{}}}", serde_json_string(&message));
119 (status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
120 }
121}
122
123fn serde_json_string(s: &str) -> String {
126 let mut out = String::with_capacity(s.len() + 2);
127 out.push('"');
128 for c in s.chars() {
129 match c {
130 '"' => out.push_str("\\\""),
131 '\\' => out.push_str("\\\\"),
132 '\n' => out.push_str("\\n"),
133 '\r' => out.push_str("\\r"),
134 '\t' => out.push_str("\\t"),
135 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
136 c => out.push(c),
137 }
138 }
139 out.push('"');
140 out
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn statuses_match_contract() {
149 assert_eq!(
150 ProxyError::OutOfScope("x".into()).status(),
151 StatusCode::FORBIDDEN
152 );
153 assert_eq!(
154 ProxyError::PreconditionFailed("x".into()).status(),
155 StatusCode::PRECONDITION_FAILED
156 );
157 assert_eq!(
158 ProxyError::RangeNotSatisfiable("x".into()).status(),
159 StatusCode::RANGE_NOT_SATISFIABLE
160 );
161 assert_eq!(
162 ProxyError::NotImplemented("gcp").status(),
163 StatusCode::NOT_IMPLEMENTED
164 );
165 }
166
167 #[test]
168 fn object_store_precondition_maps_to_412() {
169 let err = object_store::Error::Precondition {
170 path: "p".into(),
171 source: "etag mismatch".into(),
172 };
173 assert_eq!(
174 ProxyError::Storage(err).status(),
175 StatusCode::PRECONDITION_FAILED
176 );
177 }
178
179 #[test]
180 fn object_store_not_found_maps_to_404() {
181 let err = object_store::Error::NotFound {
182 path: "p".into(),
183 source: "missing".into(),
184 };
185 assert_eq!(ProxyError::Storage(err).status(), StatusCode::NOT_FOUND);
186 }
187
188 #[test]
189 fn json_escaping() {
190 assert_eq!(serde_json_string("a\"b\\c"), r#""a\"b\\c""#);
191 assert_eq!(serde_json_string("line\nbreak"), r#""line\nbreak""#);
192 }
193}