This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-xrpc / src / error.rs
14 kB 396 lines
1use axum::Json; 2use axum::response::{IntoResponse, Response}; 3use http::StatusCode; 4use serde_json::json; 5 6#[derive(Debug, Clone)] 7pub struct XrpcError { 8 status: StatusCode, 9 error: &'static str, 10 message: String, 11} 12 13impl XrpcError { 14 fn new(status: StatusCode, error: &'static str, message: impl Into<String>) -> Self { 15 Self { 16 status, 17 error, 18 message: message.into(), 19 } 20 } 21 22 pub fn invalid_request(message: impl Into<String>) -> Self { 23 Self::new(StatusCode::BAD_REQUEST, "InvalidRequest", message) 24 } 25 26 pub(crate) fn named( 27 status: StatusCode, 28 error: &'static str, 29 message: impl Into<String>, 30 ) -> Self { 31 Self::new(status, error, message) 32 } 33 34 pub fn auth_required(message: impl Into<String>) -> Self { 35 Self::new(StatusCode::UNAUTHORIZED, "AuthenticationRequired", message) 36 } 37 38 pub fn forbidden(message: impl Into<String>) -> Self { 39 Self::new(StatusCode::FORBIDDEN, "Forbidden", message) 40 } 41 42 pub fn not_found(message: impl Into<String>) -> Self { 43 Self::new(StatusCode::NOT_FOUND, "NotFound", message) 44 } 45 46 pub fn conflict(message: impl Into<String>) -> Self { 47 Self::new(StatusCode::CONFLICT, "Conflict", message) 48 } 49 50 pub fn request_too_large(message: impl Into<String>) -> Self { 51 Self::new(StatusCode::PAYLOAD_TOO_LARGE, "RequestTooLarge", message) 52 } 53 54 pub fn warming(message: impl Into<String>) -> Self { 55 Self::new( 56 StatusCode::SERVICE_UNAVAILABLE, 57 "ProjectionWarming", 58 message, 59 ) 60 } 61 62 pub fn upstream_unavailable(message: impl Into<String>) -> Self { 63 Self::new( 64 StatusCode::SERVICE_UNAVAILABLE, 65 "UpstreamUnavailable", 66 message, 67 ) 68 } 69 70 pub fn rate_limited(message: impl Into<String>) -> Self { 71 Self::new(StatusCode::TOO_MANY_REQUESTS, "RateLimitExceeded", message) 72 } 73 74 pub fn overloaded(message: impl Into<String>) -> Self { 75 Self::new(StatusCode::SERVICE_UNAVAILABLE, "Overloaded", message) 76 } 77 78 pub fn bad_gateway(message: impl Into<String>) -> Self { 79 Self::new(StatusCode::BAD_GATEWAY, "UpstreamFailure", message) 80 } 81 82 pub fn internal(message: impl Into<String>) -> Self { 83 Self::new(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", message) 84 } 85 86 pub(crate) fn status(&self) -> StatusCode { 87 self.status 88 } 89 90 pub(crate) fn from_status(status: StatusCode, message: impl Into<String>) -> Self { 91 let error = match status { 92 StatusCode::BAD_REQUEST => "InvalidRequest", 93 StatusCode::UNAUTHORIZED => "AuthenticationRequired", 94 StatusCode::FORBIDDEN => "Forbidden", 95 StatusCode::NOT_FOUND => "NotFound", 96 StatusCode::CONFLICT => "Conflict", 97 StatusCode::PAYLOAD_TOO_LARGE => "RequestTooLarge", 98 StatusCode::UNSUPPORTED_MEDIA_TYPE => "UnsupportedMediaType", 99 StatusCode::TOO_MANY_REQUESTS => "RateLimitExceeded", 100 StatusCode::SERVICE_UNAVAILABLE => "Overloaded", 101 StatusCode::BAD_GATEWAY => "UpstreamFailure", 102 _ => return Self::internal(message), 103 }; 104 Self::new(status, error, message) 105 } 106} 107 108impl std::fmt::Display for XrpcError { 109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 110 write!(f, "{}: {}", self.error, self.message) 111 } 112} 113 114impl From<knot_git::GitError> for XrpcError { 115 fn from(error: knot_git::GitError) -> Self { 116 use knot_git::GitError; 117 let message = error.to_string(); 118 match error { 119 GitError::AlreadyExists(_) => Self::conflict(message), 120 GitError::AtomicRefs(_) => Self::conflict(message), 121 GitError::UnsafeRepoDid(_) | GitError::ReservedDid(_) => Self::invalid_request(message), 122 GitError::DepthExceeded(_) => Self::invalid_request(message), 123 GitError::ArchiveTooLarge { .. } => Self::request_too_large(message), 124 GitError::Selection(_) => Self::overloaded(message), 125 // Every oid passed to the object database here came from a ref this 126 // knot already resolved or a tree it already read, so a miss means 127 // the repository is missing an object it references. A handler that 128 // looks up something the caller named reports its own named error 129 // before it ever gets an oid. 130 GitError::ObjectNotFound(_) 131 | GitError::Open { .. } 132 | GitError::Create { .. } 133 | GitError::Remove { .. } 134 | GitError::Reference { .. } 135 | GitError::Fsync { .. } 136 | GitError::Write { .. } 137 | GitError::RevWalk(_) 138 | GitError::RemoveObject { .. } 139 | GitError::Corrupt { .. } 140 | GitError::ObjectType { .. } 141 | GitError::Decode(_) 142 | GitError::Backend(_) 143 | GitError::Staging(_) 144 | GitError::Config { .. } 145 | GitError::Maintenance(_) => Self::internal(message), 146 } 147 } 148} 149 150impl From<knot_git::ApplyError> for XrpcError { 151 fn from(error: knot_git::ApplyError) -> Self { 152 use knot_git::ApplyError; 153 match error { 154 ApplyError::TooLarge => Self::request_too_large(error.to_string()), 155 ApplyError::Git(inner) => inner.into(), 156 } 157 } 158} 159 160impl From<knot_cob::CobError> for XrpcError { 161 fn from(error: knot_cob::CobError) -> Self { 162 use knot_cob::CobError; 163 match error { 164 CobError::Contended(_) | CobError::StaleTip { .. } => Self::conflict(error.to_string()), 165 other => Self::internal(other.to_string()), 166 } 167 } 168} 169 170impl From<knot_index::IndexError> for XrpcError { 171 fn from(error: knot_index::IndexError) -> Self { 172 use knot_index::IndexError; 173 match error { 174 IndexError::Git(git) => git.into(), 175 IndexError::Cob(cob) => cob.into(), 176 other => Self::internal(other.to_string()), 177 } 178 } 179} 180 181impl From<knot_secrets::SecretsError> for XrpcError { 182 fn from(error: knot_secrets::SecretsError) -> Self { 183 use knot_secrets::SecretsError; 184 match error { 185 SecretsError::Occupied(_) => Self::conflict(error.to_string()), 186 other => Self::internal(other.to_string()), 187 } 188 } 189} 190 191impl From<knot_cobs::RegistryError> for XrpcError { 192 fn from(error: knot_cobs::RegistryError) -> Self { 193 use knot_cobs::RegistryError; 194 match error { 195 RegistryError::AlreadyRegistered { .. } 196 | RegistryError::RepoMismatch { .. } 197 | RegistryError::RkeyTaken { .. } 198 | RegistryError::OwnerMoved { .. } => Self::conflict(error.to_string()), 199 RegistryError::NotRegistered { .. } | RegistryError::NotHosted { .. } => { 200 Self::not_found(error.to_string()) 201 } 202 RegistryError::Cob(cob) => cob.into(), 203 } 204 } 205} 206 207impl From<knot_atproto::AtprotoError> for XrpcError { 208 fn from(error: knot_atproto::AtprotoError) -> Self { 209 use knot_atproto::AtprotoError; 210 if error.is_transient() { 211 return Self::upstream_unavailable(error.to_string()); 212 } 213 match error { 214 AtprotoError::Resolve(_) => Self::invalid_request(error.to_string()), 215 AtprotoError::PlcSubmit { .. } => Self::bad_gateway(error.to_string()), 216 other => Self::internal(other.to_string()), 217 } 218 } 219} 220 221impl From<knot_atproto::IdentityError> for XrpcError { 222 fn from(error: knot_atproto::IdentityError) -> Self { 223 Self::internal(error.to_string()) 224 } 225} 226 227impl From<knot_pack::PackError> for XrpcError { 228 fn from(error: knot_pack::PackError) -> Self { 229 Self::from_status(error.http_status(), error.to_string()) 230 } 231} 232 233impl From<knot_pack::FetchError> for XrpcError { 234 fn from(error: knot_pack::FetchError) -> Self { 235 use knot_pack::FetchError; 236 let message = error.to_string(); 237 match error { 238 FetchError::Url(_) => Self::invalid_request(message), 239 FetchError::Network(_) => Self::upstream_unavailable(message), 240 FetchError::Status(_) | FetchError::Protocol(_) | FetchError::Remote(_) => { 241 Self::bad_gateway(message) 242 } 243 FetchError::PackTooLarge { .. } => Self::request_too_large(message), 244 FetchError::Pack(pack) => pack.into(), 245 } 246 } 247} 248 249impl IntoResponse for XrpcError { 250 fn into_response(self) -> Response { 251 match self.status { 252 StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS => tracing::warn!( 253 status = self.status.as_u16(), 254 error = self.error, 255 message = %self.message, 256 "request rejected" 257 ), 258 StatusCode::UNAUTHORIZED => tracing::debug!( 259 error = self.error, 260 message = %self.message, 261 "request unauthenticated" 262 ), 263 _ => {} 264 } 265 ( 266 self.status, 267 Json(json!({ "error": self.error, "message": self.message })), 268 ) 269 .into_response() 270 } 271} 272 273#[cfg(test)] 274mod tests { 275 use super::XrpcError; 276 use axum::response::IntoResponse; 277 use http::StatusCode; 278 279 #[test] 280 fn each_tag_maps_to_its_status_in_the_class_and_through_into_response() { 281 let cases: &[(XrpcError, StatusCode, &str)] = &[ 282 ( 283 XrpcError::invalid_request("x"), 284 StatusCode::BAD_REQUEST, 285 "InvalidRequest", 286 ), 287 ( 288 XrpcError::auth_required("x"), 289 StatusCode::UNAUTHORIZED, 290 "AuthenticationRequired", 291 ), 292 ( 293 XrpcError::forbidden("x"), 294 StatusCode::FORBIDDEN, 295 "Forbidden", 296 ), 297 (XrpcError::not_found("x"), StatusCode::NOT_FOUND, "NotFound"), 298 (XrpcError::conflict("x"), StatusCode::CONFLICT, "Conflict"), 299 ( 300 XrpcError::request_too_large("x"), 301 StatusCode::PAYLOAD_TOO_LARGE, 302 "RequestTooLarge", 303 ), 304 ( 305 XrpcError::rate_limited("x"), 306 StatusCode::TOO_MANY_REQUESTS, 307 "RateLimitExceeded", 308 ), 309 ( 310 XrpcError::warming("x"), 311 StatusCode::SERVICE_UNAVAILABLE, 312 "ProjectionWarming", 313 ), 314 ( 315 XrpcError::upstream_unavailable("x"), 316 StatusCode::SERVICE_UNAVAILABLE, 317 "UpstreamUnavailable", 318 ), 319 ( 320 XrpcError::overloaded("x"), 321 StatusCode::SERVICE_UNAVAILABLE, 322 "Overloaded", 323 ), 324 ( 325 XrpcError::bad_gateway("x"), 326 StatusCode::BAD_GATEWAY, 327 "UpstreamFailure", 328 ), 329 ( 330 XrpcError::internal("x"), 331 StatusCode::INTERNAL_SERVER_ERROR, 332 "InternalError", 333 ), 334 ]; 335 cases.iter().for_each(|(error, status, tag)| { 336 assert_eq!((error.status, error.error), (*status, *tag)); 337 assert_eq!( 338 error.clone().into_response().status(), 339 *status, 340 "into_response serves the mapped status for {tag}" 341 ); 342 }); 343 } 344 345 #[test] 346 fn a_domain_error_maps_to_the_status_that_names_whose_fault_it_is() { 347 let oid = knot_types::Oid::from_hex(&"a".repeat(40)).unwrap(); 348 let cases: Vec<(XrpcError, StatusCode, &str)> = vec![ 349 ( 350 knot_git::GitError::ObjectNotFound(oid).into(), 351 StatusCode::INTERNAL_SERVER_ERROR, 352 "these oids come from refs and trees this knot resolved itself, \ 353 and every read lexicon names its own not-found error for what the caller asked for", 354 ), 355 ( 356 knot_git::GitError::Corrupt { 357 oid, 358 message: "truncated".to_string(), 359 } 360 .into(), 361 StatusCode::INTERNAL_SERVER_ERROR, 362 "a corrupt repository on this knot isn't the caller's fault to fix", 363 ), 364 ( 365 knot_git::GitError::AtomicRefs("lost".to_string()).into(), 366 StatusCode::CONFLICT, 367 "losing a ref race is a conflict", 368 ), 369 ( 370 knot_git::GitError::AlreadyExists("/scallop".into()).into(), 371 StatusCode::CONFLICT, 372 "creating a repository that exists is a conflict", 373 ), 374 ( 375 knot_pack::FetchError::Remote("gone".to_string()).into(), 376 StatusCode::BAD_GATEWAY, 377 "a fetch failure at the upstream during fork sync is the upstream's fault", 378 ), 379 ( 380 knot_git::ApplyError::Git(knot_git::GitError::AtomicRefs("lost".to_string())) 381 .into(), 382 StatusCode::CONFLICT, 383 "wrapping a git error in ApplyError mustn't downgrade it to a generic fault", 384 ), 385 ( 386 knot_index::IndexError::Git(knot_git::GitError::AlreadyExists("/whelk".into())) 387 .into(), 388 StatusCode::CONFLICT, 389 "wrapping a git error in IndexError mustn't downgrade it either", 390 ), 391 ]; 392 cases.iter().for_each(|(error, status, why)| { 393 assert_eq!(error.status(), *status, "{why}"); 394 }); 395 } 396}