Skip to main content

systemprompt_api/error/
conversions.rs

1//! `From` impls mapping domain and repository errors onto [`ApiHttpError`],
2//! keeping the variant-to-HTTP-status mapping in one place so non-OAuth
3//! handlers use `?`. `RepositoryError` already classifies into [`ApiError`] in
4//! `systemprompt-models`; that impl is reused here. The umbrella domain errors
5//! are classified by variant so that, e.g., a repository failure surfaces as
6//! 500 while a missing entity surfaces as 404.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use systemprompt_agent::AgentError;
12use systemprompt_config::{ProfileBootstrapError, SecretsBootstrapError};
13use systemprompt_content::ContentError;
14use systemprompt_loader::{BundleError, ConfigLoadError};
15use systemprompt_marketplace::MarketplaceError;
16use systemprompt_marketplace::managed::ManagedError;
17use systemprompt_models::api::ApiError;
18use systemprompt_models::errors::GlobalConfigError;
19use systemprompt_models::execution::ContextExtractionError;
20use systemprompt_oauth::services::SessionCreationError;
21use systemprompt_oauth::{OauthError, OauthErrorKind};
22use systemprompt_security::authz::{AuthzError, ScopeBindingError};
23use systemprompt_traits::RepositoryError;
24use systemprompt_users::UserError;
25
26use super::ApiHttpError;
27
28impl From<RepositoryError> for ApiHttpError {
29    fn from(err: RepositoryError) -> Self {
30        Self(ApiError::from(err))
31    }
32}
33
34impl From<AuthzError> for ApiHttpError {
35    fn from(err: AuthzError) -> Self {
36        Self(ApiError::internal("Authorization lookup failed", err))
37    }
38}
39
40impl From<ScopeBindingError> for ApiHttpError {
41    fn from(err: ScopeBindingError) -> Self {
42        match err {
43            ScopeBindingError::UnknownDimension(_) => Self::bad_request(err.to_string()),
44            ScopeBindingError::NotAMember { .. } => Self::forbidden(err.to_string()),
45            ScopeBindingError::Lookup(source) => Self::from(source),
46        }
47    }
48}
49
50impl From<ConfigLoadError> for ApiHttpError {
51    fn from(err: ConfigLoadError) -> Self {
52        Self(ApiError::internal(
53            "Services configuration unavailable",
54            err,
55        ))
56    }
57}
58
59impl From<ProfileBootstrapError> for ApiHttpError {
60    fn from(err: ProfileBootstrapError) -> Self {
61        Self(ApiError::internal("Profile not ready", err))
62    }
63}
64
65impl From<SecretsBootstrapError> for ApiHttpError {
66    fn from(err: SecretsBootstrapError) -> Self {
67        Self(ApiError::internal("Secrets not ready", err))
68    }
69}
70
71impl From<GlobalConfigError> for ApiHttpError {
72    fn from(err: GlobalConfigError) -> Self {
73        Self(ApiError::internal("Configuration not ready", err))
74    }
75}
76
77impl From<AgentError> for ApiHttpError {
78    fn from(err: AgentError) -> Self {
79        let api = match err {
80            AgentError::NotFound(msg) => ApiError::not_found(msg),
81            AgentError::Repository(inner) => ApiError::from(inner),
82            other => ApiError::internal("Agent operation failed", other),
83        };
84        Self(api)
85    }
86}
87
88impl From<ContentError> for ApiHttpError {
89    fn from(err: ContentError) -> Self {
90        let api = match err {
91            ContentError::Repository(inner) => ApiError::from(inner),
92            e @ (ContentError::ContentNotFound(_) | ContentError::LinkNotFound(_)) => {
93                ApiError::not_found(e.to_string())
94            },
95            e @ (ContentError::InvalidRequest(_) | ContentError::Validation(_)) => {
96                ApiError::bad_request(e.to_string())
97            },
98            other => ApiError::internal("Content operation failed", other),
99        };
100        Self(api)
101    }
102}
103
104impl From<MarketplaceError> for ApiHttpError {
105    fn from(err: MarketplaceError) -> Self {
106        let api = match err {
107            MarketplaceError::Managed(ManagedError::Repository(inner)) => ApiError::from(inner),
108            e @ (MarketplaceError::NotFound(_)
109            | MarketplaceError::NoDefault
110            | MarketplaceError::Managed(ManagedError::Unavailable)) => {
111                ApiError::not_found(e.to_string())
112            },
113            e @ MarketplaceError::Managed(
114                ManagedError::Invalid(_) | ManagedError::InvalidInput { .. },
115            ) => ApiError::bad_request(e.to_string()),
116            e @ MarketplaceError::Managed(ManagedError::Conflict(_)) => {
117                ApiError::conflict(e.to_string())
118            },
119            e @ (MarketplaceError::Catalog(_)
120            | MarketplaceError::CatalogSource { .. }
121            | MarketplaceError::Managed(_)
122            | MarketplaceError::Import { .. }
123            | MarketplaceError::ImportSource { .. }
124            | MarketplaceError::Signing(_)
125            | MarketplaceError::Filter(_)) => ApiError::internal("Marketplace operation failed", e),
126        };
127        Self(api)
128    }
129}
130
131impl From<UserError> for ApiHttpError {
132    fn from(err: UserError) -> Self {
133        let api = match err {
134            UserError::Repository(inner) => ApiError::from(inner),
135            e @ UserError::NotFound(_) => ApiError::not_found(e.to_string()),
136            e @ (UserError::EmailAlreadyExists(_)
137            | UserError::LegalHold(_)
138            | UserError::NotArchived(_)
139            | UserError::RestoreRefused { .. }) => ApiError::conflict(e.to_string()),
140            e @ (UserError::Validation(_)
141            | UserError::InvalidStatus(_)
142            | UserError::InvalidRole(_)
143            | UserError::InvalidRoles(_)) => ApiError::bad_request(e.to_string()),
144            e @ (UserError::MergeUnavailable
145            | UserError::PurgeIdentifier { .. }
146            | UserError::OwnerReassignment { .. }) => {
147                ApiError::internal("User operation failed", e)
148            },
149        };
150        Self(api)
151    }
152}
153
154impl From<OauthError> for ApiHttpError {
155    fn from(err: OauthError) -> Self {
156        if matches!(
157            err,
158            OauthError::CodeNotFound(_)
159                | OauthError::TokenNotFound(_)
160                | OauthError::ClientNotFound(_)
161                | OauthError::UserNotFound(_)
162        ) {
163            return Self(ApiError::not_found(err.to_string()));
164        }
165        let api = match err.kind() {
166            OauthErrorKind::InvalidRequest
167            | OauthErrorKind::InvalidClientMetadata
168            | OauthErrorKind::ExpiredChallenge
169            | OauthErrorKind::InvalidCredential => ApiError::bad_request(err.to_string()),
170            OauthErrorKind::UsernameUnavailable | OauthErrorKind::EmailExists => {
171                ApiError::conflict(err.to_string())
172            },
173            OauthErrorKind::InvalidClient => ApiError::unauthorized("Client authentication failed"),
174            OauthErrorKind::AuthenticationFailed => ApiError::unauthorized("Authentication failed"),
175            OauthErrorKind::InvalidGrant
176            | OauthErrorKind::InvalidToken
177            | OauthErrorKind::AccessDenied => ApiError::unauthorized(err.to_string()),
178            OauthErrorKind::NotFound => ApiError::not_found(err.to_string()),
179            OauthErrorKind::ServerError => {
180                ApiError::internal("Authorization operation failed", err)
181            },
182        };
183        Self(api)
184    }
185}
186
187impl From<SessionCreationError> for ApiHttpError {
188    fn from(err: SessionCreationError) -> Self {
189        Self(match err {
190            e @ SessionCreationError::UserNotFound { .. } => ApiError::not_found(e.to_string()),
191            other => ApiError::internal("Session creation failed", other),
192        })
193    }
194}
195
196impl From<ContextExtractionError> for ApiHttpError {
197    fn from(err: ContextExtractionError) -> Self {
198        let api = match err {
199            ContextExtractionError::InvalidToken(source) => {
200                ApiError::unauthorized("Invalid or expired token").with_source(source)
201            },
202            e @ (ContextExtractionError::MissingHeader(_)
203            | ContextExtractionError::MissingAuthHeader
204            | ContextExtractionError::Revoked
205            | ContextExtractionError::MissingSessionId
206            | ContextExtractionError::MissingUserId) => ApiError::unauthorized(e.to_string()),
207            e @ (ContextExtractionError::MissingContextId
208            | ContextExtractionError::InvalidHeaderValue { .. }
209            | ContextExtractionError::InvalidUserId(_)) => ApiError::bad_request(e.to_string()),
210            e @ ContextExtractionError::ForbiddenHeader { .. } => {
211                ApiError::forbidden(e.to_string())
212            },
213            e @ ContextExtractionError::UserNotFound(_) => ApiError::not_found(e.to_string()),
214            e @ ContextExtractionError::DatabaseError { .. } => {
215                ApiError::internal("Request context lookup failed", e)
216            },
217        };
218        Self(api)
219    }
220}
221
222impl From<BundleError> for ApiHttpError {
223    fn from(err: BundleError) -> Self {
224        let api = match err {
225            e @ BundleError::Auth { .. } => ApiError::forbidden(e.to_string()),
226            e @ (BundleError::Verify(_)
227            | BundleError::Ownership { .. }
228            | BundleError::Policy { .. }
229            | BundleError::TooLarge { .. }) => ApiError::bad_request(e.to_string()),
230            e @ BundleError::SourceMissing { .. } => ApiError::not_found(e.to_string()),
231            e @ (BundleError::Fetch { .. } | BundleError::Extract { .. } | BundleError::Io(_)) => {
232                ApiError::internal("Services bundle operation failed", e)
233            },
234        };
235        Self(api)
236    }
237}