Skip to main content

hyphae_server/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::{io, net::SocketAddr};
4
5use axum::{
6    Json,
7    http::{HeaderValue, StatusCode, header},
8    response::{IntoResponse, Response},
9};
10use hyphae_contracts::v1::ErrorV1;
11use hyphae_engine::{EngineError, ProofError};
12use hyphae_query::QueryError;
13use hyphae_storage::{LogError, MutationError, StorageError};
14use thiserror::Error;
15
16use crate::ServerConfigError;
17
18/// Failure before or while running the optional HTTP delivery surface.
19#[derive(Debug, Error)]
20pub enum ServerError {
21    /// Secure configuration validation failed before socket bind.
22    #[error(transparent)]
23    Configuration(#[from] ServerConfigError),
24    /// The exclusively owned embedded engine could not open.
25    #[error("failed to open Hyphae engine: {0}")]
26    Engine(#[from] EngineError),
27    /// The requested socket could not be bound.
28    #[error("failed to bind Hyphae server at {address}: {source}")]
29    Bind {
30        /// Requested listener address.
31        address: SocketAddr,
32        /// Operating-system failure.
33        #[source]
34        source: io::Error,
35    },
36    /// The bound HTTP service failed.
37    #[error("Hyphae HTTP service failed: {0}")]
38    Serve(#[source] io::Error),
39}
40
41#[derive(Clone, Debug)]
42pub(crate) struct ApiError {
43    status: StatusCode,
44    code: &'static str,
45    message: &'static str,
46    request_id: String,
47}
48
49impl ApiError {
50    pub(crate) fn new(
51        status: StatusCode,
52        code: &'static str,
53        message: &'static str,
54        request_id: impl Into<String>,
55    ) -> Self {
56        Self {
57            status,
58            code,
59            message,
60            request_id: request_id.into(),
61        }
62    }
63
64    pub(crate) fn invalid(request_id: &str) -> Self {
65        Self::new(
66            StatusCode::BAD_REQUEST,
67            "invalid_request",
68            "request does not satisfy the version 1 contract",
69            request_id,
70        )
71    }
72
73    pub(crate) fn limit(request_id: &str) -> Self {
74        Self::new(
75            StatusCode::UNPROCESSABLE_ENTITY,
76            "limit_exceeded",
77            "request exceeds an enforced server limit",
78            request_id,
79        )
80    }
81
82    pub(crate) fn payload_too_large(request_id: &str) -> Self {
83        Self::new(
84            StatusCode::PAYLOAD_TOO_LARGE,
85            "payload_too_large",
86            "request or response byte budget exceeded",
87            request_id,
88        )
89    }
90
91    pub(crate) fn result_too_large(request_id: &str) -> Self {
92        Self::new(
93            StatusCode::PAYLOAD_TOO_LARGE,
94            "result_too_large",
95            "proof-bearing result exceeds an enforced byte limit",
96            request_id,
97        )
98    }
99
100    pub(crate) fn internal(request_id: &str) -> Self {
101        Self::new(
102            StatusCode::INTERNAL_SERVER_ERROR,
103            "internal_error",
104            "internal operation failed; inspect local server diagnostics",
105            request_id,
106        )
107    }
108
109    pub(crate) fn unavailable(request_id: &str) -> Self {
110        Self::new(
111            StatusCode::SERVICE_UNAVAILABLE,
112            "unavailable",
113            "owned engine requires local recovery before serving data operations",
114            request_id,
115        )
116    }
117
118    pub(crate) fn from_engine(error: EngineError, request_id: &str) -> Self {
119        match error {
120            EngineError::DuplicateDocumentKey => Self::invalid(request_id),
121            EngineError::Document(_) => Self::limit(request_id),
122            EngineError::Query(source) => Self::from_query(&source, request_id),
123            EngineError::Storage(source) => Self::from_storage(&source, request_id),
124            EngineError::Proof(ProofError::ProofLimitExceeded { .. }) => {
125                Self::result_too_large(request_id)
126            }
127            EngineError::Backup(_) | EngineError::Proof(_) | EngineError::Retrieval(_) => {
128                Self::internal(request_id)
129            }
130        }
131    }
132
133    fn from_query(error: &QueryError, request_id: &str) -> Self {
134        match error {
135            QueryError::TimedOut => Self::new(
136                StatusCode::REQUEST_TIMEOUT,
137                "timeout",
138                "query deadline elapsed without a partial result",
139                request_id,
140            ),
141            QueryError::ResultLimitExceeded { .. }
142            | QueryError::FilterNodesExceeded { .. }
143            | QueryError::FilterDepthExceeded { .. }
144            | QueryError::SortFieldsExceeded { .. }
145            | QueryError::GroupFieldsExceeded { .. }
146            | QueryError::MetricsExceeded { .. }
147            | QueryError::ScannedBudgetExceeded { .. }
148            | QueryError::MatchedBudgetExceeded { .. }
149            | QueryError::GroupBudgetExceeded { .. } => Self::limit(request_id),
150            QueryError::EmptyRecordKey
151            | QueryError::DuplicateRecordKey
152            | QueryError::ZeroLimit
153            | QueryError::CursorShape { .. }
154            | QueryError::EmptyCursorKey
155            | QueryError::NoncanonicalCursorNull
156            | QueryError::InvalidPrefixType
157            | QueryError::InvalidFieldPath
158            | QueryError::EmptyMetricName
159            | QueryError::DuplicateMetricName { .. }
160            | QueryError::MetricTypeMismatch { .. }
161            | QueryError::ArithmeticOverflow { .. }
162            | QueryError::MetricStateMismatch => Self::invalid(request_id),
163        }
164    }
165
166    fn from_storage(error: &StorageError, request_id: &str) -> Self {
167        match error {
168            StorageError::Mutation(
169                MutationError::EmptyKey
170                | MutationError::KeyTooLarge { .. }
171                | MutationError::OperationTooLarge { .. },
172            )
173            | StorageError::Log(
174                LogError::EmptyTransaction
175                | LogError::TooManyOperations
176                | LogError::PayloadTooLarge { .. },
177            ) => Self::limit(request_id),
178            StorageError::Log(LogError::IdempotencyConflict { .. }) => Self::new(
179                StatusCode::CONFLICT,
180                "idempotency_conflict",
181                "transaction identifier was already committed with different contents",
182                request_id,
183            ),
184            _ => Self::internal(request_id),
185        }
186    }
187}
188
189impl IntoResponse for ApiError {
190    fn into_response(self) -> Response {
191        let envelope = ErrorV1 {
192            code: self.code.to_owned(),
193            message: self.message.to_owned(),
194            request_id: self.request_id,
195        };
196        let mut response = (self.status, Json(envelope)).into_response();
197        if self.status == StatusCode::UNAUTHORIZED {
198            response.headers_mut().insert(
199                header::WWW_AUTHENTICATE,
200                HeaderValue::from_static("Bearer realm=\"hyphae\""),
201            );
202        }
203        if self.status == StatusCode::TOO_MANY_REQUESTS {
204            response
205                .headers_mut()
206                .insert(header::RETRY_AFTER, HeaderValue::from_static("1"));
207        }
208        response
209    }
210}