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 395 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::Selection(_) => Self::overloaded(message), 124 // Every oid passed to the object database here came from a ref this 125 // knot already resolved or a tree it already read, so a miss means 126 // the repository is missing an object it references. A handler that 127 // looks up something the caller named reports its own named error 128 // before it ever gets an oid. 129 GitError::ObjectNotFound(_) 130 | GitError::Open { .. } 131 | GitError::Create { .. } 132 | GitError::Remove { .. } 133 | GitError::Reference { .. } 134 | GitError::Fsync { .. } 135 | GitError::Write { .. } 136 | GitError::RevWalk(_) 137 | GitError::RemoveObject { .. } 138 | GitError::Corrupt { .. } 139 | GitError::ObjectType { .. } 140 | GitError::Decode(_) 141 | GitError::Backend(_) 142 | GitError::Staging(_) 143 | GitError::Config { .. } 144 | GitError::Maintenance(_) => Self::internal(message), 145 } 146 } 147} 148 149impl From<knot_git::ApplyError> for XrpcError { 150 fn from(error: knot_git::ApplyError) -> Self { 151 use knot_git::ApplyError; 152 match error { 153 ApplyError::TooLarge => Self::request_too_large(error.to_string()), 154 ApplyError::Git(inner) => inner.into(), 155 } 156 } 157} 158 159impl From<knot_cob::CobError> for XrpcError { 160 fn from(error: knot_cob::CobError) -> Self { 161 use knot_cob::CobError; 162 match error { 163 CobError::Contended(_) | CobError::StaleTip { .. } => Self::conflict(error.to_string()), 164 other => Self::internal(other.to_string()), 165 } 166 } 167} 168 169impl From<knot_index::IndexError> for XrpcError { 170 fn from(error: knot_index::IndexError) -> Self { 171 use knot_index::IndexError; 172 match error { 173 IndexError::Git(git) => git.into(), 174 IndexError::Cob(cob) => cob.into(), 175 other => Self::internal(other.to_string()), 176 } 177 } 178} 179 180impl From<knot_secrets::SecretsError> for XrpcError { 181 fn from(error: knot_secrets::SecretsError) -> Self { 182 use knot_secrets::SecretsError; 183 match error { 184 SecretsError::Occupied(_) => Self::conflict(error.to_string()), 185 other => Self::internal(other.to_string()), 186 } 187 } 188} 189 190impl From<knot_cobs::RegistryError> for XrpcError { 191 fn from(error: knot_cobs::RegistryError) -> Self { 192 use knot_cobs::RegistryError; 193 match error { 194 RegistryError::AlreadyRegistered { .. } 195 | RegistryError::RepoMismatch { .. } 196 | RegistryError::RkeyTaken { .. } 197 | RegistryError::OwnerMoved { .. } => Self::conflict(error.to_string()), 198 RegistryError::NotRegistered { .. } | RegistryError::NotHosted { .. } => { 199 Self::not_found(error.to_string()) 200 } 201 RegistryError::Cob(cob) => cob.into(), 202 } 203 } 204} 205 206impl From<knot_atproto::AtprotoError> for XrpcError { 207 fn from(error: knot_atproto::AtprotoError) -> Self { 208 use knot_atproto::AtprotoError; 209 if error.is_transient() { 210 return Self::upstream_unavailable(error.to_string()); 211 } 212 match error { 213 AtprotoError::Resolve(_) => Self::invalid_request(error.to_string()), 214 AtprotoError::PlcSubmit { .. } => Self::bad_gateway(error.to_string()), 215 other => Self::internal(other.to_string()), 216 } 217 } 218} 219 220impl From<knot_atproto::IdentityError> for XrpcError { 221 fn from(error: knot_atproto::IdentityError) -> Self { 222 Self::internal(error.to_string()) 223 } 224} 225 226impl From<knot_pack::PackError> for XrpcError { 227 fn from(error: knot_pack::PackError) -> Self { 228 Self::from_status(error.http_status(), error.to_string()) 229 } 230} 231 232impl From<knot_pack::FetchError> for XrpcError { 233 fn from(error: knot_pack::FetchError) -> Self { 234 use knot_pack::FetchError; 235 let message = error.to_string(); 236 match error { 237 FetchError::Url(_) => Self::invalid_request(message), 238 FetchError::Network(_) => Self::upstream_unavailable(message), 239 FetchError::Status(_) | FetchError::Protocol(_) | FetchError::Remote(_) => { 240 Self::bad_gateway(message) 241 } 242 FetchError::PackTooLarge { .. } => Self::request_too_large(message), 243 FetchError::Pack(pack) => pack.into(), 244 } 245 } 246} 247 248impl IntoResponse for XrpcError { 249 fn into_response(self) -> Response { 250 match self.status { 251 StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS => tracing::warn!( 252 status = self.status.as_u16(), 253 error = self.error, 254 message = %self.message, 255 "request rejected" 256 ), 257 StatusCode::UNAUTHORIZED => tracing::debug!( 258 error = self.error, 259 message = %self.message, 260 "request unauthenticated" 261 ), 262 _ => {} 263 } 264 ( 265 self.status, 266 Json(json!({ "error": self.error, "message": self.message })), 267 ) 268 .into_response() 269 } 270} 271 272#[cfg(test)] 273mod tests { 274 use super::XrpcError; 275 use axum::response::IntoResponse; 276 use http::StatusCode; 277 278 #[test] 279 fn each_tag_maps_to_its_status_in_the_class_and_through_into_response() { 280 let cases: &[(XrpcError, StatusCode, &str)] = &[ 281 ( 282 XrpcError::invalid_request("x"), 283 StatusCode::BAD_REQUEST, 284 "InvalidRequest", 285 ), 286 ( 287 XrpcError::auth_required("x"), 288 StatusCode::UNAUTHORIZED, 289 "AuthenticationRequired", 290 ), 291 ( 292 XrpcError::forbidden("x"), 293 StatusCode::FORBIDDEN, 294 "Forbidden", 295 ), 296 (XrpcError::not_found("x"), StatusCode::NOT_FOUND, "NotFound"), 297 (XrpcError::conflict("x"), StatusCode::CONFLICT, "Conflict"), 298 ( 299 XrpcError::request_too_large("x"), 300 StatusCode::PAYLOAD_TOO_LARGE, 301 "RequestTooLarge", 302 ), 303 ( 304 XrpcError::rate_limited("x"), 305 StatusCode::TOO_MANY_REQUESTS, 306 "RateLimitExceeded", 307 ), 308 ( 309 XrpcError::warming("x"), 310 StatusCode::SERVICE_UNAVAILABLE, 311 "ProjectionWarming", 312 ), 313 ( 314 XrpcError::upstream_unavailable("x"), 315 StatusCode::SERVICE_UNAVAILABLE, 316 "UpstreamUnavailable", 317 ), 318 ( 319 XrpcError::overloaded("x"), 320 StatusCode::SERVICE_UNAVAILABLE, 321 "Overloaded", 322 ), 323 ( 324 XrpcError::bad_gateway("x"), 325 StatusCode::BAD_GATEWAY, 326 "UpstreamFailure", 327 ), 328 ( 329 XrpcError::internal("x"), 330 StatusCode::INTERNAL_SERVER_ERROR, 331 "InternalError", 332 ), 333 ]; 334 cases.iter().for_each(|(error, status, tag)| { 335 assert_eq!((error.status, error.error), (*status, *tag)); 336 assert_eq!( 337 error.clone().into_response().status(), 338 *status, 339 "into_response serves the mapped status for {tag}" 340 ); 341 }); 342 } 343 344 #[test] 345 fn a_domain_error_maps_to_the_status_that_names_whose_fault_it_is() { 346 let oid = knot_types::Oid::from_hex(&"a".repeat(40)).unwrap(); 347 let cases: Vec<(XrpcError, StatusCode, &str)> = vec![ 348 ( 349 knot_git::GitError::ObjectNotFound(oid).into(), 350 StatusCode::INTERNAL_SERVER_ERROR, 351 "these oids come from refs and trees this knot resolved itself, \ 352 and every read lexicon names its own not-found error for what the caller asked for", 353 ), 354 ( 355 knot_git::GitError::Corrupt { 356 oid, 357 message: "truncated".to_string(), 358 } 359 .into(), 360 StatusCode::INTERNAL_SERVER_ERROR, 361 "a corrupt repository on this knot isn't the caller's fault to fix", 362 ), 363 ( 364 knot_git::GitError::AtomicRefs("lost".to_string()).into(), 365 StatusCode::CONFLICT, 366 "losing a ref race is a conflict", 367 ), 368 ( 369 knot_git::GitError::AlreadyExists("/scallop".into()).into(), 370 StatusCode::CONFLICT, 371 "creating a repository that exists is a conflict", 372 ), 373 ( 374 knot_pack::FetchError::Remote("gone".to_string()).into(), 375 StatusCode::BAD_GATEWAY, 376 "a fetch failure at the upstream during fork sync is the upstream's fault", 377 ), 378 ( 379 knot_git::ApplyError::Git(knot_git::GitError::AtomicRefs("lost".to_string())) 380 .into(), 381 StatusCode::CONFLICT, 382 "wrapping a git error in ApplyError mustn't downgrade it to a generic fault", 383 ), 384 ( 385 knot_index::IndexError::Git(knot_git::GitError::AlreadyExists("/whelk".into())) 386 .into(), 387 StatusCode::CONFLICT, 388 "wrapping a git error in IndexError mustn't downgrade it either", 389 ), 390 ]; 391 cases.iter().for_each(|(error, status, why)| { 392 assert_eq!(error.status(), *status, "{why}"); 393 }); 394 } 395}