Skip to main content

kinetic_core/
api_error.rs

1//! RFC 7807 Problem Details HTTP API error serialization boundary.
2//!
3//! `ApiError` is the single unified error payload that crosses HTTP network boundaries.
4//! All domain-specific error enums in [`crate::error`] implement `From<T> for ApiError`,
5//! mapping internal failures to RFC 7807 Problem Details JSON format with Kinetic extensions.
6
7use crate::error::{
8    DnsError, DrandError, GovernanceError, IdentityError, NamesError, NetworkClientError,
9    PublishError, RegistrationError, ResolutionError, StorageError, VdfError,
10};
11use serde::{Deserialize, Serialize};
12
13/// RFC 7807 Problem Details representation for HTTP API responses, augmented with Kinetic extensions.
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct ApiError {
16    /// RFC 7807: URI identifying the specific error category (e.g. `"https://kinetic.network/errors/KIN-RES-002"`).
17    #[serde(rename = "type")]
18    pub error_type: String,
19    /// RFC 7807: Short human-readable title summarizing the error category.
20    pub title: String,
21    /// RFC 7807: Associated HTTP response status code (e.g. `404`, `503`).
22    pub status: u16,
23    /// RFC 7807: Human-facing explanation of the specific error occurrence.
24    pub detail: String,
25    /// RFC 7807: Optional URI identifying the specific request instance.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub instance: Option<String>,
28    /// Kinetic Extension: Stable protocol error code (e.g. `"KIN-RES-002"`).
29    pub code: String,
30    /// Kinetic Extension: Indicates whether client applications should retry the request.
31    pub retryable: bool,
32    /// Kinetic Extension: Developer-facing structured JSON diagnostic details.
33    #[serde(skip_serializing_if = "serde_json::Value::is_null")]
34    pub details: serde_json::Value,
35    /// Kinetic Extension: Task-local correlation ID for server log tracing.
36    pub request_id: String,
37}
38
39impl ApiError {
40    /// Returns the HTTP status code associated with this error.
41    pub fn http_status(&self) -> u16 {
42        self.status
43    }
44}
45
46fn current_request_id() -> String {
47    crate::request_id::current().to_string()
48}
49
50impl From<ResolutionError> for ApiError {
51    fn from(e: ResolutionError) -> Self {
52        let (status, title): (u16, &'static str) = match &e {
53            ResolutionError::Offline => (503, "Node Offline"),
54            ResolutionError::NotFound { .. } => (404, "Name Not Found"),
55            ResolutionError::VdfVerificationFailed { .. } => {
56                (422, "Cryptographic Verification Failed")
57            }
58            ResolutionError::Expired { .. } => (410, "Registration Expired"),
59            ResolutionError::Timeout { .. } => (504, "Resolution Timeout"),
60            ResolutionError::Internal { .. } => (500, "Internal Resolution Error"),
61        };
62        ApiError {
63            error_type: e.error_type_uri(),
64            title: title.to_string(),
65            status,
66            detail: e.user_message(),
67            instance: None,
68            code: e.code().to_string(),
69            retryable: e.is_retryable(),
70            details: e.details(),
71            request_id: current_request_id(),
72        }
73    }
74}
75
76impl From<PublishError> for ApiError {
77    fn from(e: PublishError) -> Self {
78        let (status, title): (u16, &'static str) = match &e {
79            PublishError::Offline => (503, "Node Offline"),
80            PublishError::InvalidProof(_) => (400, "Invalid VDF Proof"),
81            PublishError::AlreadyOwned { .. } => (409, "Name Already Owned"),
82            PublishError::AllFailed { .. } => (503, "Publish Failed"),
83            PublishError::Rejected(_) => (422, "Publish Rejected"),
84            PublishError::Internal { .. } => (500, "Internal Publish Error"),
85        };
86        ApiError {
87            error_type: e.error_type_uri(),
88            title: title.to_string(),
89            status,
90            detail: e.user_message(),
91            instance: None,
92            code: e.code().to_string(),
93            retryable: e.is_retryable(),
94            details: e.details(),
95            request_id: current_request_id(),
96        }
97    }
98}
99
100impl From<RegistrationError> for ApiError {
101    fn from(e: RegistrationError) -> Self {
102        let (status, title): (u16, &'static str) = match &e {
103            RegistrationError::InvalidName { .. } => (400, "Invalid Name"),
104            RegistrationError::VdfFailed(_) => (500, "VDF Computation Failed"),
105            RegistrationError::CommitmentMismatch => (422, "Commitment Mismatch"),
106            RegistrationError::AlreadyOwned { .. } => (409, "Name Already Owned"),
107            RegistrationError::AlreadyInProgress { .. } => (409, "Registration In Progress"),
108            RegistrationError::NetworkRejected { .. } => (422, "Registration Rejected"),
109            RegistrationError::Internal { .. } => (500, "Internal Registration Error"),
110        };
111        ApiError {
112            error_type: e.error_type_uri(),
113            title: title.to_string(),
114            status,
115            detail: e.user_message(),
116            instance: None,
117            code: e.code().to_string(),
118            retryable: e.is_retryable(),
119            details: e.details(),
120            request_id: current_request_id(),
121        }
122    }
123}
124
125impl From<GovernanceError> for ApiError {
126    fn from(e: GovernanceError) -> Self {
127        let (status, title): (u16, &'static str) = match &e {
128            GovernanceError::MissingRootKey => (500, "Configuration Error"),
129            GovernanceError::StaleProposal
130            | GovernanceError::TimelockNotExpired
131            | GovernanceError::NotPendingOrVetoed => (409, "Conflict"),
132            GovernanceError::InsufficientSignatures => (401, "Unauthorized"),
133            GovernanceError::GovernanceDisabled => (403, "Forbidden"),
134            GovernanceError::KeyLengthMismatch | GovernanceError::InvalidPremiumNameLength | GovernanceError::InvalidInfrastructureName => {
135                (400, "Bad Request")
136            }
137        };
138        ApiError {
139            error_type: e.error_type_uri(),
140            title: title.to_string(),
141            status,
142            detail: e.user_message(),
143            instance: None,
144            code: e.code().to_string(),
145            retryable: e.is_retryable(),
146            details: serde_json::Value::Null,
147            request_id: current_request_id(),
148        }
149    }
150}
151
152impl From<NetworkClientError> for ApiError {
153    fn from(e: NetworkClientError) -> Self {
154        let (status, title): (u16, &'static str) = match &e {
155            NetworkClientError::Timeout | NetworkClientError::StreamDropped => {
156                (504, "Gateway Timeout")
157            }
158            NetworkClientError::Offline | NetworkClientError::RoutingTableEmpty => {
159                (503, "Service Unavailable")
160            }
161            NetworkClientError::ChannelClosed
162            | NetworkClientError::StoreError(_)
163            | NetworkClientError::Other(_) => (500, "Internal Server Error"),
164            NetworkClientError::UnsupportedProtocol => (501, "Not Implemented"),
165            NetworkClientError::GossipSubError(_) => (502, "Bad Gateway"),
166        };
167        ApiError {
168            error_type: e.error_type_uri(),
169            title: title.to_string(),
170            status,
171            detail: e.user_message(),
172            instance: None,
173            code: e.code().to_string(),
174            retryable: e.is_retryable(),
175            details: serde_json::Value::Null,
176            request_id: current_request_id(),
177        }
178    }
179}
180
181impl From<StorageError> for ApiError {
182    fn from(e: StorageError) -> Self {
183        let (status, title): (u16, &'static str) = match &e {
184            StorageError::DatabaseLocked => (423, "Locked"),
185            StorageError::Corruption(_) => (500, "Storage Corruption"),
186            StorageError::OperationFailed(_) => (500, "Storage Operation Failed"),
187        };
188        ApiError {
189            error_type: e.error_type_uri(),
190            title: title.to_string(),
191            status,
192            detail: e.user_message(),
193            instance: None,
194            code: e.code().to_string(),
195            retryable: e.is_retryable(),
196            details: serde_json::Value::Null,
197            request_id: current_request_id(),
198        }
199    }
200}
201
202impl From<VdfError> for ApiError {
203    fn from(e: VdfError) -> Self {
204        let (status, title): (u16, &'static str) = match &e {
205            VdfError::LockFileError(_) | VdfError::LockAcquireError(_) => {
206                (503, "Service Unavailable")
207            }
208            VdfError::DiscriminantError | VdfError::ProofGenerationError => {
209                (500, "VDF Computation Error")
210            }
211            VdfError::UnsupportedPlatform => (501, "Not Implemented"),
212            VdfError::InvalidProof => (400, "Bad Request"),
213        };
214        ApiError {
215            error_type: e.error_type_uri(),
216            title: title.to_string(),
217            status,
218            detail: e.user_message(),
219            instance: None,
220            code: e.code().to_string(),
221            retryable: e.is_retryable(),
222            details: serde_json::Value::Null,
223            request_id: current_request_id(),
224        }
225    }
226}
227
228impl From<DrandError> for ApiError {
229    fn from(e: DrandError) -> Self {
230        let (status, title): (u16, &'static str) = match &e {
231            DrandError::AllEndpointsFailed | DrandError::Network(_) | DrandError::Reqwest(_) => {
232                (502, "Bad Gateway")
233            }
234            DrandError::HttpError(s) => (*s, "Upstream Error"),
235            DrandError::NoCachedKyn => (404, "Not Found"),
236            DrandError::Serde(_) | DrandError::Storage(_) => (500, "Internal Server Error"),
237            DrandError::InvalidSignature => (422, "Cryptographic Verification Failed"),
238            DrandError::StaleKyn { .. } => (400, "Stale Network Kyn"),
239        };
240        ApiError {
241            error_type: e.error_type_uri(),
242            title: title.to_string(),
243            status,
244            detail: e.user_message(),
245            instance: None,
246            code: e.code().to_string(),
247            retryable: e.is_retryable(),
248            details: serde_json::Value::Null,
249            request_id: current_request_id(),
250        }
251    }
252}
253
254impl From<DnsError> for ApiError {
255    fn from(e: DnsError) -> Self {
256        let (status, title): (u16, &'static str) = match &e {
257            DnsError::NestedTooDeeply
258            | DnsError::ParseError(_)
259            | DnsError::TooManyRecords
260            | DnsError::InvalidLabelLength(_)
261            | DnsError::InvalidLabelCharacters(_)
262            | DnsError::InvalidCnameConfiguration(_)
263            | DnsError::TxtRecordTooLong(_)
264            | DnsError::InvalidCnameTarget(_)
265            | DnsError::InvalidPeerId(_)
266            | DnsError::InvalidKid(_)
267            | DnsError::InvalidIpfsCid(_) => (400, "Bad Request"),
268        };
269        ApiError {
270            error_type: e.error_type_uri(),
271            title: title.to_string(),
272            status,
273            detail: e.user_message(),
274            instance: None,
275            code: e.code().to_string(),
276            retryable: e.is_retryable(),
277            details: serde_json::Value::Null,
278            request_id: current_request_id(),
279        }
280    }
281}
282
283impl From<IdentityError> for ApiError {
284    fn from(e: IdentityError) -> Self {
285        let (status, title): (u16, &'static str) = match &e {
286            IdentityError::Io(_) | IdentityError::CorruptedIdentityFile(_) => {
287                (500, "Internal Server Error")
288            }
289            IdentityError::IdentityNotFound(_) => (404, "Not Found"),
290            IdentityError::InvalidSeedPhrase(_) => (400, "Bad Request"),
291            IdentityError::DecryptionFailed(_) => (401, "Unauthorized"),
292        };
293        ApiError {
294            error_type: e.error_type_uri(),
295            title: title.to_string(),
296            status,
297            detail: e.user_message(),
298            instance: None,
299            code: e.code().to_string(),
300            retryable: e.is_retryable(),
301            details: serde_json::Value::Null,
302            request_id: current_request_id(),
303        }
304    }
305}
306
307impl From<NamesError> for ApiError {
308    fn from(e: NamesError) -> Self {
309        // All NamesError variants are deterministic input validation failures — 400 Bad Request.
310        ApiError {
311            error_type: e.error_type_uri(),
312            title: "Invalid Name".to_string(),
313            status: 400,
314            detail: e.user_message(),
315            instance: None,
316            code: e.code().to_string(),
317            retryable: e.is_retryable(),
318            details: serde_json::Value::Null,
319            request_id: current_request_id(),
320        }
321    }
322}