this repo has no description
1use axum::{
2 Json,
3 extract::{FromRequest, Request, rejection::JsonRejection},
4 http::StatusCode,
5 response::{IntoResponse, Response},
6};
7use serde::{Serialize, de::DeserializeOwned};
8use std::borrow::Cow;
9
10#[derive(Debug, Serialize)]
11struct ErrorBody<'a> {
12 error: Cow<'a, str>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 message: Option<String>,
15}
16
17#[derive(Debug)]
18pub enum ApiError {
19 InternalError(Option<String>),
20 AuthenticationRequired,
21 AuthenticationFailed(Option<String>),
22 InvalidRequest(String),
23 InvalidToken(Option<String>),
24 ExpiredToken(Option<String>),
25 TokenRequired,
26 AccountDeactivated,
27 AccountTakedown,
28 AccountNotFound,
29 RepoNotFound(Option<String>),
30 RepoTakendown,
31 RepoDeactivated,
32 RecordNotFound,
33 BlobNotFound(Option<String>),
34 InvalidHandle(Option<String>),
35 HandleNotAvailable(Option<String>),
36 HandleTaken,
37 InvalidEmail,
38 EmailTaken,
39 InvalidInviteCode,
40 DuplicateCreate,
41 DuplicateAppPassword,
42 AppPasswordNotFound,
43 SessionNotFound,
44 InvalidSwap(Option<String>),
45 InvalidPassword(String),
46 InvalidRepo(String),
47 AccountMigrated,
48 AccountNotVerified,
49 InvalidCollection,
50 InvalidRecord(String),
51 Forbidden,
52 AdminRequired,
53 InsufficientScope(Option<String>),
54 InvitesDisabled,
55 RateLimitExceeded(Option<String>),
56 PayloadTooLarge(String),
57 TotpAlreadyEnabled,
58 TotpNotEnabled,
59 InvalidCode(Option<String>),
60 InvalidChannel,
61 IdentifierMismatch,
62 NoPasskeys,
63 NoChallengeInProgress,
64 InvalidCredential,
65 PasskeyCounterAnomaly,
66 NoRegistrationInProgress,
67 RegistrationFailed,
68 PasskeyNotFound,
69 InvalidId,
70 InvalidScopes(String),
71 ControllerNotFound,
72 InvalidDelegation(String),
73 DelegationNotFound,
74 InviteCodeRequired,
75 BackupNotFound,
76 BackupsDisabled,
77 RepoNotReady,
78 DeviceNotFound,
79 NoEmail,
80 MfaVerificationRequired,
81 AuthorizationError(String),
82 InvalidDid(String),
83 InvalidSigningKey,
84 SetupExpired,
85 InvalidAccount,
86 InvalidRecoveryLink,
87 RecoveryLinkExpired,
88 MissingEmail,
89 MissingDiscordId,
90 MissingTelegramUsername,
91 MissingSignalNumber,
92 InvalidVerificationChannel,
93 SelfHostedDidWebDisabled,
94 AccountAlreadyExists,
95 HandleNotFound,
96 SubjectNotFound,
97 NotFoundMsg(String),
98 ServiceUnavailable(Option<String>),
99 UpstreamErrorMsg(String),
100 DatabaseError,
101 UpstreamFailure,
102 UpstreamTimeout,
103 UpstreamUnavailable(String),
104 UpstreamError {
105 status: u16,
106 error: Option<String>,
107 message: Option<String>,
108 },
109}
110
111impl ApiError {
112 fn status_code(&self) -> StatusCode {
113 match self {
114 Self::InternalError(_) | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
115 Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => {
116 StatusCode::BAD_GATEWAY
117 }
118 Self::ServiceUnavailable(_) | Self::BackupsDisabled => StatusCode::SERVICE_UNAVAILABLE,
119 Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT,
120 Self::UpstreamError { status, .. } => {
121 StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY)
122 }
123 Self::AuthenticationRequired
124 | Self::AuthenticationFailed(_)
125 | Self::AccountDeactivated
126 | Self::AccountTakedown
127 | Self::InvalidCode(_)
128 | Self::InvalidPassword(_)
129 | Self::InvalidToken(_)
130 | Self::ExpiredToken(_)
131 | Self::PasskeyCounterAnomaly => StatusCode::UNAUTHORIZED,
132 Self::Forbidden
133 | Self::AdminRequired
134 | Self::InsufficientScope(_)
135 | Self::InvitesDisabled
136 | Self::InvalidRepo(_)
137 | Self::AccountMigrated
138 | Self::AccountNotVerified
139 | Self::MfaVerificationRequired
140 | Self::AuthorizationError(_) => StatusCode::FORBIDDEN,
141 Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
142 Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
143 Self::AccountNotFound
144 | Self::RecordNotFound
145 | Self::AppPasswordNotFound
146 | Self::SessionNotFound
147 | Self::DeviceNotFound
148 | Self::ControllerNotFound
149 | Self::DelegationNotFound
150 | Self::BackupNotFound
151 | Self::InvalidRecoveryLink
152 | Self::HandleNotFound
153 | Self::SubjectNotFound
154 | Self::BlobNotFound(_)
155 | Self::NotFoundMsg(_) => StatusCode::NOT_FOUND,
156 Self::RepoTakendown | Self::RepoDeactivated | Self::RepoNotFound(_) => {
157 StatusCode::BAD_REQUEST
158 }
159 Self::InvalidSwap(_) | Self::TotpAlreadyEnabled => StatusCode::CONFLICT,
160 Self::InvalidRequest(_)
161 | Self::InvalidHandle(_)
162 | Self::HandleNotAvailable(_)
163 | Self::HandleTaken
164 | Self::InvalidEmail
165 | Self::EmailTaken
166 | Self::InvalidInviteCode
167 | Self::DuplicateCreate
168 | Self::DuplicateAppPassword
169 | Self::InvalidCollection
170 | Self::InvalidRecord(_)
171 | Self::TotpNotEnabled
172 | Self::InvalidChannel
173 | Self::IdentifierMismatch
174 | Self::NoPasskeys
175 | Self::NoChallengeInProgress
176 | Self::InvalidCredential
177 | Self::NoEmail
178 | Self::NoRegistrationInProgress
179 | Self::RegistrationFailed
180 | Self::InvalidId
181 | Self::InvalidScopes(_)
182 | Self::InvalidDelegation(_)
183 | Self::InviteCodeRequired
184 | Self::RepoNotReady
185 | Self::InvalidDid(_)
186 | Self::InvalidSigningKey
187 | Self::SetupExpired
188 | Self::InvalidAccount
189 | Self::RecoveryLinkExpired
190 | Self::MissingEmail
191 | Self::MissingDiscordId
192 | Self::MissingTelegramUsername
193 | Self::MissingSignalNumber
194 | Self::InvalidVerificationChannel
195 | Self::SelfHostedDidWebDisabled
196 | Self::AccountAlreadyExists
197 | Self::TokenRequired => StatusCode::BAD_REQUEST,
198 Self::PasskeyNotFound => StatusCode::NOT_FOUND,
199 }
200 }
201 fn error_name(&self) -> Cow<'static, str> {
202 match self {
203 Self::InternalError(_) | Self::DatabaseError => Cow::Borrowed("InternalError"),
204 Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => {
205 Cow::Borrowed("UpstreamError")
206 }
207 Self::ServiceUnavailable(_) => Cow::Borrowed("ServiceUnavailable"),
208 Self::NotFoundMsg(_) => Cow::Borrowed("NotFound"),
209 Self::UpstreamTimeout => Cow::Borrowed("UpstreamTimeout"),
210 Self::UpstreamError { error, .. } => {
211 if let Some(e) = error {
212 return Cow::Owned(e.clone());
213 }
214 Cow::Borrowed("UpstreamError")
215 }
216 Self::AuthenticationRequired => Cow::Borrowed("AuthenticationRequired"),
217 Self::AuthenticationFailed(_) => Cow::Borrowed("AuthenticationFailed"),
218 Self::InvalidToken(_) => Cow::Borrowed("InvalidToken"),
219 Self::ExpiredToken(_) => Cow::Borrowed("ExpiredToken"),
220 Self::TokenRequired => Cow::Borrowed("TokenRequired"),
221 Self::AccountDeactivated => Cow::Borrowed("AccountDeactivated"),
222 Self::AccountTakedown => Cow::Borrowed("AccountTakedown"),
223 Self::Forbidden => Cow::Borrowed("Forbidden"),
224 Self::AdminRequired => Cow::Borrowed("AdminRequired"),
225 Self::InsufficientScope(_) => Cow::Borrowed("InsufficientScope"),
226 Self::InvitesDisabled => Cow::Borrowed("InvitesDisabled"),
227 Self::AccountNotFound => Cow::Borrowed("AccountNotFound"),
228 Self::RepoNotFound(_) => Cow::Borrowed("RepoNotFound"),
229 Self::RepoTakendown => Cow::Borrowed("RepoTakendown"),
230 Self::RepoDeactivated => Cow::Borrowed("RepoDeactivated"),
231 Self::RecordNotFound => Cow::Borrowed("RecordNotFound"),
232 Self::BlobNotFound(_) => Cow::Borrowed("BlobNotFound"),
233 Self::AppPasswordNotFound => Cow::Borrowed("AppPasswordNotFound"),
234 Self::SessionNotFound => Cow::Borrowed("SessionNotFound"),
235 Self::InvalidRequest(_) => Cow::Borrowed("InvalidRequest"),
236 Self::InvalidHandle(_) => Cow::Borrowed("InvalidHandle"),
237 Self::HandleNotAvailable(_) => Cow::Borrowed("HandleNotAvailable"),
238 Self::HandleTaken => Cow::Borrowed("HandleTaken"),
239 Self::InvalidEmail => Cow::Borrowed("InvalidEmail"),
240 Self::EmailTaken => Cow::Borrowed("EmailTaken"),
241 Self::InvalidInviteCode => Cow::Borrowed("InvalidInviteCode"),
242 Self::DuplicateCreate => Cow::Borrowed("DuplicateCreate"),
243 Self::DuplicateAppPassword => Cow::Borrowed("DuplicateAppPassword"),
244 Self::InvalidSwap(_) => Cow::Borrowed("InvalidSwap"),
245 Self::InvalidPassword(_) => Cow::Borrowed("InvalidPassword"),
246 Self::InvalidRepo(_) => Cow::Borrowed("InvalidRepo"),
247 Self::AccountMigrated => Cow::Borrowed("AccountMigrated"),
248 Self::AccountNotVerified => Cow::Borrowed("AccountNotVerified"),
249 Self::InvalidCollection => Cow::Borrowed("InvalidCollection"),
250 Self::InvalidRecord(_) => Cow::Borrowed("InvalidRecord"),
251 Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"),
252 Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"),
253 Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"),
254 Self::InvalidChannel => Cow::Borrowed("InvalidChannel"),
255 Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"),
256 Self::NoPasskeys => Cow::Borrowed("NoPasskeys"),
257 Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"),
258 Self::InvalidCredential => Cow::Borrowed("InvalidCredential"),
259 Self::PasskeyCounterAnomaly => Cow::Borrowed("PasskeyCounterAnomaly"),
260 Self::NoRegistrationInProgress => Cow::Borrowed("NoRegistrationInProgress"),
261 Self::RegistrationFailed => Cow::Borrowed("RegistrationFailed"),
262 Self::PasskeyNotFound => Cow::Borrowed("PasskeyNotFound"),
263 Self::InvalidId => Cow::Borrowed("InvalidId"),
264 Self::InvalidScopes(_) => Cow::Borrowed("InvalidScopes"),
265 Self::ControllerNotFound => Cow::Borrowed("ControllerNotFound"),
266 Self::InvalidDelegation(_) => Cow::Borrowed("InvalidDelegation"),
267 Self::DelegationNotFound => Cow::Borrowed("DelegationNotFound"),
268 Self::InviteCodeRequired => Cow::Borrowed("InviteCodeRequired"),
269 Self::BackupNotFound => Cow::Borrowed("BackupNotFound"),
270 Self::BackupsDisabled => Cow::Borrowed("BackupsDisabled"),
271 Self::RepoNotReady => Cow::Borrowed("RepoNotReady"),
272 Self::MfaVerificationRequired => Cow::Borrowed("MfaVerificationRequired"),
273 Self::RateLimitExceeded(_) => Cow::Borrowed("RateLimitExceeded"),
274 Self::PayloadTooLarge(_) => Cow::Borrowed("PayloadTooLarge"),
275 Self::DeviceNotFound => Cow::Borrowed("DeviceNotFound"),
276 Self::NoEmail => Cow::Borrowed("NoEmail"),
277 Self::AuthorizationError(_) => Cow::Borrowed("AuthorizationError"),
278 Self::InvalidDid(_) => Cow::Borrowed("InvalidDid"),
279 Self::InvalidSigningKey => Cow::Borrowed("InvalidSigningKey"),
280 Self::SetupExpired => Cow::Borrowed("SetupExpired"),
281 Self::InvalidAccount => Cow::Borrowed("InvalidAccount"),
282 Self::InvalidRecoveryLink => Cow::Borrowed("InvalidRecoveryLink"),
283 Self::RecoveryLinkExpired => Cow::Borrowed("RecoveryLinkExpired"),
284 Self::MissingEmail => Cow::Borrowed("MissingEmail"),
285 Self::MissingDiscordId => Cow::Borrowed("MissingDiscordId"),
286 Self::MissingTelegramUsername => Cow::Borrowed("MissingTelegramUsername"),
287 Self::MissingSignalNumber => Cow::Borrowed("MissingSignalNumber"),
288 Self::InvalidVerificationChannel => Cow::Borrowed("InvalidVerificationChannel"),
289 Self::SelfHostedDidWebDisabled => Cow::Borrowed("SelfHostedDidWebDisabled"),
290 Self::AccountAlreadyExists => Cow::Borrowed("AccountAlreadyExists"),
291 Self::HandleNotFound => Cow::Borrowed("HandleNotFound"),
292 Self::SubjectNotFound => Cow::Borrowed("SubjectNotFound"),
293 }
294 }
295 fn message(&self) -> Option<String> {
296 match self {
297 Self::InternalError(msg)
298 | Self::AuthenticationFailed(msg)
299 | Self::InvalidToken(msg)
300 | Self::ExpiredToken(msg)
301 | Self::RepoNotFound(msg)
302 | Self::BlobNotFound(msg)
303 | Self::InvalidHandle(msg)
304 | Self::HandleNotAvailable(msg)
305 | Self::InvalidSwap(msg)
306 | Self::InsufficientScope(msg)
307 | Self::InvalidCode(msg)
308 | Self::RateLimitExceeded(msg)
309 | Self::ServiceUnavailable(msg) => msg.clone(),
310 Self::InvalidRequest(msg)
311 | Self::UpstreamUnavailable(msg)
312 | Self::InvalidPassword(msg)
313 | Self::InvalidRepo(msg)
314 | Self::InvalidRecord(msg)
315 | Self::NotFoundMsg(msg)
316 | Self::UpstreamErrorMsg(msg)
317 | Self::PayloadTooLarge(msg) => Some(msg.clone()),
318 Self::AccountMigrated => Some(
319 "Account has been migrated to another PDS. Repo operations are not allowed."
320 .to_string(),
321 ),
322 Self::AccountNotVerified => Some(
323 "You must verify at least one notification channel before creating records"
324 .to_string(),
325 ),
326 Self::NoPasskeys => {
327 Some("No passkeys registered for this account".to_string())
328 }
329 Self::NoChallengeInProgress => Some(
330 "No passkey authentication in progress or challenge expired".to_string(),
331 ),
332 Self::InvalidCredential => Some("Failed to parse credential response".to_string()),
333 Self::NoRegistrationInProgress => Some(
334 "No registration in progress. Call startPasskeyRegistration first.".to_string(),
335 ),
336 Self::RegistrationFailed => {
337 Some("Failed to verify passkey registration".to_string())
338 }
339 Self::PasskeyNotFound => Some("Passkey not found".to_string()),
340 Self::InvalidId => Some("Invalid ID format".to_string()),
341 Self::InvalidScopes(msg) | Self::InvalidDelegation(msg) => Some(msg.clone()),
342 Self::ControllerNotFound => Some("Controller account not found".to_string()),
343 Self::DelegationNotFound => {
344 Some("No active delegation found for this controller".to_string())
345 }
346 Self::InviteCodeRequired => {
347 Some("An invite code is required to create an account".to_string())
348 }
349 Self::BackupNotFound => Some("Backup not found".to_string()),
350 Self::BackupsDisabled => Some("Backup storage not configured".to_string()),
351 Self::RepoNotReady => Some("Repository not ready for backup".to_string()),
352 Self::PasskeyCounterAnomaly => Some(
353 "Authentication failed: security key counter anomaly detected. This may indicate a cloned key.".to_string(),
354 ),
355 Self::MfaVerificationRequired => Some(
356 "This sensitive operation requires MFA verification".to_string(),
357 ),
358 Self::DeviceNotFound => Some("Device not found".to_string()),
359 Self::NoEmail => Some("Recipient has no email address".to_string()),
360 Self::AuthorizationError(msg) | Self::InvalidDid(msg) => Some(msg.clone()),
361 Self::InvalidSigningKey => {
362 Some("Signing key not found, already used, or expired".to_string())
363 }
364 Self::SetupExpired => {
365 Some("Setup has already been completed or expired".to_string())
366 }
367 Self::InvalidAccount => {
368 Some("This account is not a passkey-only account".to_string())
369 }
370 Self::InvalidRecoveryLink => Some("Invalid recovery link".to_string()),
371 Self::RecoveryLinkExpired => Some("Recovery link has expired".to_string()),
372 Self::MissingEmail => {
373 Some("Email is required when using email verification".to_string())
374 }
375 Self::MissingDiscordId => {
376 Some("Discord ID is required when using Discord verification".to_string())
377 }
378 Self::MissingTelegramUsername => {
379 Some("Telegram username is required when using Telegram verification".to_string())
380 }
381 Self::MissingSignalNumber => {
382 Some("Signal phone number is required when using Signal verification".to_string())
383 }
384 Self::InvalidVerificationChannel => Some("Invalid verification channel".to_string()),
385 Self::SelfHostedDidWebDisabled => {
386 Some("Self-hosted did:web accounts are disabled on this server".to_string())
387 }
388 Self::AccountAlreadyExists => Some("Account already exists".to_string()),
389 Self::HandleNotFound => Some("Unable to resolve handle".to_string()),
390 Self::SubjectNotFound => Some("Subject not found".to_string()),
391 Self::IdentifierMismatch => {
392 Some("The identifier does not match the verification token".to_string())
393 }
394 Self::UpstreamError { message, .. } => message.clone(),
395 Self::UpstreamTimeout => Some("Upstream service timed out".to_string()),
396 Self::AdminRequired => Some("This action requires admin privileges".to_string()),
397 _ => None,
398 }
399 }
400 pub fn from_upstream_response(status: u16, body: &[u8]) -> Self {
401 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(body) {
402 let error = parsed
403 .get("error")
404 .and_then(|v| v.as_str())
405 .map(String::from);
406 let message = parsed
407 .get("message")
408 .and_then(|v| v.as_str())
409 .map(String::from);
410 return Self::UpstreamError {
411 status,
412 error,
413 message,
414 };
415 }
416 Self::UpstreamError {
417 status,
418 error: None,
419 message: None,
420 }
421 }
422}
423
424impl IntoResponse for ApiError {
425 fn into_response(self) -> Response {
426 let body = ErrorBody {
427 error: self.error_name(),
428 message: self.message(),
429 };
430 (self.status_code(), Json(body)).into_response()
431 }
432}
433
434impl From<sqlx::Error> for ApiError {
435 fn from(e: sqlx::Error) -> Self {
436 tracing::error!("Database error: {:?}", e);
437 Self::DatabaseError
438 }
439}
440
441impl From<crate::auth::TokenValidationError> for ApiError {
442 fn from(e: crate::auth::TokenValidationError) -> Self {
443 match e {
444 crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated,
445 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown,
446 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError(None),
447 crate::auth::TokenValidationError::AuthenticationFailed => {
448 Self::AuthenticationFailed(None)
449 }
450 crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken(None),
451 }
452 }
453}
454
455impl From<crate::util::DbLookupError> for ApiError {
456 fn from(e: crate::util::DbLookupError) -> Self {
457 match e {
458 crate::util::DbLookupError::NotFound => Self::AccountNotFound,
459 crate::util::DbLookupError::DatabaseError(db_err) => {
460 tracing::error!("Database error: {:?}", db_err);
461 Self::DatabaseError
462 }
463 }
464 }
465}
466
467impl From<crate::auth::extractor::AuthError> for ApiError {
468 fn from(e: crate::auth::extractor::AuthError) -> Self {
469 match e {
470 crate::auth::extractor::AuthError::MissingToken => Self::AuthenticationRequired,
471 crate::auth::extractor::AuthError::InvalidFormat => {
472 Self::AuthenticationFailed(Some("Invalid authorization header format".to_string()))
473 }
474 crate::auth::extractor::AuthError::AuthenticationFailed => {
475 Self::AuthenticationFailed(None)
476 }
477 crate::auth::extractor::AuthError::TokenExpired => {
478 Self::ExpiredToken(Some("Token has expired".to_string()))
479 }
480 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated,
481 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown,
482 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired,
483 }
484 }
485}
486
487impl From<crate::handle::HandleResolutionError> for ApiError {
488 fn from(e: crate::handle::HandleResolutionError) -> Self {
489 match e {
490 crate::handle::HandleResolutionError::NotFound => Self::HandleNotFound,
491 crate::handle::HandleResolutionError::InvalidDid => {
492 Self::InvalidHandle(Some("Invalid DID format in handle record".to_string()))
493 }
494 crate::handle::HandleResolutionError::DidMismatch { expected, actual } => {
495 Self::InvalidHandle(Some(format!(
496 "Handle DID mismatch: expected {}, got {}",
497 expected, actual
498 )))
499 }
500 crate::handle::HandleResolutionError::DnsError(msg) => {
501 Self::InternalError(Some(format!("DNS resolution failed: {}", msg)))
502 }
503 crate::handle::HandleResolutionError::HttpError(msg) => {
504 Self::InternalError(Some(format!("Handle HTTP resolution failed: {}", msg)))
505 }
506 }
507 }
508}
509
510impl From<crate::auth::verification_token::VerifyError> for ApiError {
511 fn from(e: crate::auth::verification_token::VerifyError) -> Self {
512 use crate::auth::verification_token::VerifyError;
513 match e {
514 VerifyError::InvalidFormat => {
515 Self::InvalidRequest("The verification code is invalid or malformed".to_string())
516 }
517 VerifyError::UnsupportedVersion => {
518 Self::InvalidRequest("This verification code version is not supported".to_string())
519 }
520 VerifyError::Expired => Self::InvalidRequest(
521 "The verification code has expired. Please request a new one.".to_string(),
522 ),
523 VerifyError::InvalidSignature => {
524 Self::InvalidRequest("The verification code is invalid".to_string())
525 }
526 VerifyError::IdentifierMismatch => Self::IdentifierMismatch,
527 VerifyError::PurposeMismatch => {
528 Self::InvalidRequest("Verification code purpose does not match".to_string())
529 }
530 VerifyError::ChannelMismatch => {
531 Self::InvalidRequest("Verification code channel does not match".to_string())
532 }
533 }
534 }
535}
536
537impl From<crate::api::validation::HandleValidationError> for ApiError {
538 fn from(e: crate::api::validation::HandleValidationError) -> Self {
539 use crate::api::validation::HandleValidationError;
540 match e {
541 HandleValidationError::Reserved => Self::HandleNotAvailable(None),
542 HandleValidationError::BannedWord => {
543 Self::InvalidHandle(Some("Inappropriate language in handle".to_string()))
544 }
545 _ => Self::InvalidHandle(Some(e.to_string())),
546 }
547 }
548}
549
550impl From<jacquard::types::string::AtStrError> for ApiError {
551 fn from(e: jacquard::types::string::AtStrError) -> Self {
552 Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind))
553 }
554}
555
556impl From<crate::plc::PlcError> for ApiError {
557 fn from(e: crate::plc::PlcError) -> Self {
558 use crate::plc::PlcError;
559 match e {
560 PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()),
561 PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()),
562 PlcError::Timeout => Self::UpstreamTimeout,
563 PlcError::CircuitBreakerOpen => Self::ServiceUnavailable(Some(
564 "PLC directory service temporarily unavailable".into(),
565 )),
566 PlcError::Http(err) => {
567 tracing::error!("PLC HTTP error: {:?}", err);
568 Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into())
569 }
570 PlcError::InvalidResponse(msg) => {
571 tracing::error!("PLC invalid response: {}", msg);
572 Self::UpstreamErrorMsg(format!("Invalid response from PLC directory: {}", msg))
573 }
574 PlcError::Serialization(msg) => {
575 tracing::error!("PLC serialization error: {}", msg);
576 Self::InternalError(Some(format!("PLC serialization error: {}", msg)))
577 }
578 PlcError::Signing(msg) => {
579 tracing::error!("PLC signing error: {}", msg);
580 Self::InternalError(Some(format!("PLC signing error: {}", msg)))
581 }
582 }
583 }
584}
585
586impl From<bcrypt::BcryptError> for ApiError {
587 fn from(e: bcrypt::BcryptError) -> Self {
588 tracing::error!("Bcrypt error: {:?}", e);
589 Self::InternalError(None)
590 }
591}
592
593impl From<cid::Error> for ApiError {
594 fn from(e: cid::Error) -> Self {
595 Self::InvalidRequest(format!("Invalid CID: {}", e))
596 }
597}
598
599impl From<crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>> for ApiError {
600 fn from(e: crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>) -> Self {
601 use crate::circuit_breaker::CircuitBreakerError;
602 match e {
603 CircuitBreakerError::CircuitOpen(err) => {
604 tracing::warn!("PLC directory circuit breaker open: {}", err);
605 Self::ServiceUnavailable(Some(
606 "PLC directory service temporarily unavailable".into(),
607 ))
608 }
609 CircuitBreakerError::OperationFailed(plc_err) => Self::from(plc_err),
610 }
611 }
612}
613
614impl From<crate::storage::StorageError> for ApiError {
615 fn from(e: crate::storage::StorageError) -> Self {
616 tracing::error!("Storage error: {:?}", e);
617 Self::InternalError(Some("Storage operation failed".into()))
618 }
619}
620
621pub struct AtpJson<T>(pub T);
622
623impl<T, S> FromRequest<S> for AtpJson<T>
624where
625 T: DeserializeOwned,
626 S: Send + Sync,
627{
628 type Rejection = (StatusCode, Json<serde_json::Value>);
629
630 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
631 match Json::<T>::from_request(req, state).await {
632 Ok(Json(value)) => Ok(AtpJson(value)),
633 Err(rejection) => {
634 let message = extract_json_error_message(&rejection);
635 Err((
636 StatusCode::BAD_REQUEST,
637 Json(serde_json::json!({
638 "error": "InvalidRequest",
639 "message": message
640 })),
641 ))
642 }
643 }
644 }
645}
646
647fn extract_json_error_message(rejection: &JsonRejection) -> String {
648 match rejection {
649 JsonRejection::JsonDataError(e) => {
650 let inner = e.body_text();
651 if inner.contains("missing field") {
652 let field = inner
653 .split("missing field `")
654 .nth(1)
655 .and_then(|s| s.split('`').next())
656 .unwrap_or("unknown");
657 format!("Missing required field: {}", field)
658 } else if inner.contains("invalid type") {
659 format!("Invalid field type: {}", inner)
660 } else {
661 inner
662 }
663 }
664 JsonRejection::JsonSyntaxError(_) => "Invalid JSON syntax".to_string(),
665 JsonRejection::MissingJsonContentType(_) => {
666 "Content-Type must be application/json".to_string()
667 }
668 JsonRejection::BytesRejection(_) => "Failed to read request body".to_string(),
669 _ => "Invalid request body".to_string(),
670 }
671}