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(_) => ApiError::conflict(e.to_string()),
137            e @ (UserError::Validation(_)
138            | UserError::InvalidStatus(_)
139            | UserError::InvalidRole(_)
140            | UserError::InvalidRoles(_)) => ApiError::bad_request(e.to_string()),
141            e @ (UserError::MergeUnavailable
142            | UserError::PurgeIdentifier { .. }
143            | UserError::OwnerReassignment { .. }) => {
144                ApiError::internal("User operation failed", e)
145            },
146        };
147        Self(api)
148    }
149}
150
151impl From<OauthError> for ApiHttpError {
152    fn from(err: OauthError) -> Self {
153        if matches!(
154            err,
155            OauthError::CodeNotFound(_)
156                | OauthError::TokenNotFound(_)
157                | OauthError::ClientNotFound(_)
158                | OauthError::UserNotFound(_)
159        ) {
160            return Self(ApiError::not_found(err.to_string()));
161        }
162        let api = match err.kind() {
163            OauthErrorKind::InvalidRequest
164            | OauthErrorKind::InvalidClientMetadata
165            | OauthErrorKind::ExpiredChallenge
166            | OauthErrorKind::InvalidCredential => ApiError::bad_request(err.to_string()),
167            OauthErrorKind::UsernameUnavailable | OauthErrorKind::EmailExists => {
168                ApiError::conflict(err.to_string())
169            },
170            OauthErrorKind::InvalidClient => ApiError::unauthorized("Client authentication failed"),
171            OauthErrorKind::AuthenticationFailed => ApiError::unauthorized("Authentication failed"),
172            OauthErrorKind::InvalidGrant
173            | OauthErrorKind::InvalidToken
174            | OauthErrorKind::AccessDenied => ApiError::unauthorized(err.to_string()),
175            OauthErrorKind::NotFound => ApiError::not_found(err.to_string()),
176            OauthErrorKind::ServerError => {
177                ApiError::internal("Authorization operation failed", err)
178            },
179        };
180        Self(api)
181    }
182}
183
184impl From<SessionCreationError> for ApiHttpError {
185    fn from(err: SessionCreationError) -> Self {
186        Self(match err {
187            e @ SessionCreationError::UserNotFound { .. } => ApiError::not_found(e.to_string()),
188            other => ApiError::internal("Session creation failed", other),
189        })
190    }
191}
192
193impl From<ContextExtractionError> for ApiHttpError {
194    fn from(err: ContextExtractionError) -> Self {
195        let api = match err {
196            ContextExtractionError::InvalidToken(source) => {
197                ApiError::unauthorized("Invalid or expired token").with_source(source)
198            },
199            e @ (ContextExtractionError::MissingHeader(_)
200            | ContextExtractionError::MissingAuthHeader
201            | ContextExtractionError::Revoked
202            | ContextExtractionError::MissingSessionId
203            | ContextExtractionError::MissingUserId) => ApiError::unauthorized(e.to_string()),
204            e @ (ContextExtractionError::MissingContextId
205            | ContextExtractionError::InvalidHeaderValue { .. }
206            | ContextExtractionError::InvalidUserId(_)) => ApiError::bad_request(e.to_string()),
207            e @ ContextExtractionError::ForbiddenHeader { .. } => {
208                ApiError::forbidden(e.to_string())
209            },
210            e @ ContextExtractionError::UserNotFound(_) => ApiError::not_found(e.to_string()),
211            e @ ContextExtractionError::DatabaseError { .. } => {
212                ApiError::internal("Request context lookup failed", e)
213            },
214        };
215        Self(api)
216    }
217}
218
219impl From<BundleError> for ApiHttpError {
220    fn from(err: BundleError) -> Self {
221        let api = match err {
222            e @ BundleError::Auth { .. } => ApiError::forbidden(e.to_string()),
223            e @ (BundleError::Verify(_)
224            | BundleError::Ownership { .. }
225            | BundleError::Policy { .. }
226            | BundleError::TooLarge { .. }) => ApiError::bad_request(e.to_string()),
227            e @ BundleError::SourceMissing { .. } => ApiError::not_found(e.to_string()),
228            e @ (BundleError::Fetch { .. } | BundleError::Extract { .. } | BundleError::Io(_)) => {
229                ApiError::internal("Services bundle operation failed", e)
230            },
231        };
232        Self(api)
233    }
234}