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::{BoundedEngineQueryError, EngineError, ProofError, RetrievalProofError};
12use hyphae_query::QueryError;
13use hyphae_retrieval::{ExactRetrievalError, HybridError, LexicalError};
14use hyphae_storage::{
15    LogError, MaterializedIndexError, MutationError, SnapshotError, StorageError,
16    StorageLimitError, storage_limit_from_io,
17};
18use thiserror::Error;
19
20use crate::ServerConfigError;
21
22/// Failure before or while running the optional HTTP delivery surface.
23#[derive(Debug, Error)]
24pub enum ServerError {
25    /// Secure configuration validation failed before socket bind.
26    #[error(transparent)]
27    Configuration(#[from] ServerConfigError),
28    /// The exclusively owned embedded engine could not open.
29    #[error("failed to open Hyphae engine: {0}")]
30    Engine(#[from] EngineError),
31    /// The requested socket could not be bound.
32    #[error("failed to bind Hyphae server at {address}: {source}")]
33    Bind {
34        /// Requested listener address.
35        address: SocketAddr,
36        /// Operating-system failure.
37        #[source]
38        source: io::Error,
39    },
40    /// The bound HTTP service failed.
41    #[error("Hyphae HTTP service failed: {0}")]
42    Serve(#[source] io::Error),
43}
44
45#[derive(Clone, Debug)]
46pub(crate) struct ApiError {
47    status: StatusCode,
48    code: &'static str,
49    message: &'static str,
50    request_id: String,
51}
52
53impl ApiError {
54    pub(crate) fn new(
55        status: StatusCode,
56        code: &'static str,
57        message: &'static str,
58        request_id: impl Into<String>,
59    ) -> Self {
60        Self {
61            status,
62            code,
63            message,
64            request_id: request_id.into(),
65        }
66    }
67
68    pub(crate) fn invalid(request_id: &str) -> Self {
69        Self::new(
70            StatusCode::BAD_REQUEST,
71            "invalid_request",
72            "request does not satisfy the version 1 contract",
73            request_id,
74        )
75    }
76
77    pub(crate) fn limit(request_id: &str) -> Self {
78        Self::new(
79            StatusCode::UNPROCESSABLE_ENTITY,
80            "limit_exceeded",
81            "request exceeds an enforced server limit",
82            request_id,
83        )
84    }
85
86    pub(crate) fn payload_too_large(request_id: &str) -> Self {
87        Self::new(
88            StatusCode::PAYLOAD_TOO_LARGE,
89            "payload_too_large",
90            "request or response byte budget exceeded",
91            request_id,
92        )
93    }
94
95    pub(crate) fn timeout(request_id: &str) -> Self {
96        Self::new(
97            StatusCode::REQUEST_TIMEOUT,
98            "timeout",
99            "operation deadline elapsed without a partial result",
100            request_id,
101        )
102    }
103
104    pub(crate) fn result_too_large(request_id: &str) -> Self {
105        Self::new(
106            StatusCode::PAYLOAD_TOO_LARGE,
107            "result_too_large",
108            "proof-bearing result exceeds an enforced byte limit",
109            request_id,
110        )
111    }
112
113    pub(crate) fn internal(request_id: &str) -> Self {
114        Self::new(
115            StatusCode::INTERNAL_SERVER_ERROR,
116            "internal_error",
117            "internal operation failed; inspect local server diagnostics",
118            request_id,
119        )
120    }
121
122    pub(crate) fn unavailable(request_id: &str) -> Self {
123        Self::new(
124            StatusCode::SERVICE_UNAVAILABLE,
125            "unavailable",
126            "owned engine requires local recovery before serving data operations",
127            request_id,
128        )
129    }
130
131    pub(crate) fn from_engine(error: EngineError, request_id: &str) -> Self {
132        match error {
133            EngineError::DuplicateDocumentKey | EngineError::EmptyBatch => {
134                Self::invalid(request_id)
135            }
136            EngineError::Document(_) => Self::limit(request_id),
137            EngineError::Query(source) => Self::from_query(&source, request_id),
138            EngineError::Storage(source) => Self::from_storage(&source, request_id),
139            EngineError::ExactRetrieval(source) => Self::from_exact_retrieval(&source, request_id),
140            EngineError::Lexical(source) => Self::from_lexical(&source, request_id),
141            EngineError::Hybrid(source) => Self::from_hybrid(&source, request_id),
142            EngineError::Proof(ProofError::ProofLimitExceeded { .. })
143            | EngineError::RetrievalProof(RetrievalProofError::ProofLimitExceeded { .. }) => {
144                Self::result_too_large(request_id)
145            }
146            EngineError::Backup(_)
147            | EngineError::Proof(_)
148            | EngineError::RetrievalProof(_)
149            | EngineError::Retrieval(_) => Self::internal(request_id),
150        }
151    }
152
153    pub(crate) fn from_bounded_query(error: BoundedEngineQueryError, request_id: &str) -> Self {
154        match error {
155            BoundedEngineQueryError::Engine(source) => Self::from_engine(source, request_id),
156            BoundedEngineQueryError::ScannedByteBudgetExceeded { .. } => Self::limit(request_id),
157        }
158    }
159
160    fn from_lexical(error: &LexicalError, request_id: &str) -> Self {
161        match error {
162            LexicalError::TimedOut => Self::new(
163                StatusCode::REQUEST_TIMEOUT,
164                "timeout",
165                "lexical retrieval deadline elapsed without a partial result",
166                request_id,
167            ),
168            LexicalError::ResultLimitExceeded { .. }
169            | LexicalError::DocumentBudgetExceeded { .. }
170            | LexicalError::TokenBudgetExceeded { .. }
171            | LexicalError::CandidateBudgetExceeded { .. } => Self::limit(request_id),
172            LexicalError::EmptyFields
173            | LexicalError::TooManyFields
174            | LexicalError::EmptyFieldPath
175            | LexicalError::InvalidFieldSegment
176            | LexicalError::DuplicateFieldPath
177            | LexicalError::InvalidFieldWeight
178            | LexicalError::IndexMismatch
179            | LexicalError::EmptyQuery
180            | LexicalError::ZeroLimit
181            | LexicalError::EmptyDocumentKey
182            | LexicalError::DuplicateDocumentKey => Self::invalid(request_id),
183            LexicalError::ArithmeticOverflow | LexicalError::MalformedProjection => {
184                Self::internal(request_id)
185            }
186        }
187    }
188
189    fn from_hybrid(error: &HybridError, request_id: &str) -> Self {
190        match error {
191            HybridError::InvalidWeight
192            | HybridError::ZeroLimit
193            | HybridError::DuplicateBranchKey => Self::invalid(request_id),
194            HybridError::ArithmeticOverflow => Self::internal(request_id),
195        }
196    }
197
198    fn from_exact_retrieval(error: &ExactRetrievalError, request_id: &str) -> Self {
199        match error {
200            ExactRetrievalError::TimedOut => Self::new(
201                StatusCode::REQUEST_TIMEOUT,
202                "timeout",
203                "retrieval deadline elapsed without a partial result",
204                request_id,
205            ),
206            ExactRetrievalError::ResultLimitExceeded { .. }
207            | ExactRetrievalError::CandidateBudgetExceeded { .. }
208            | ExactRetrievalError::CandidateByteBudgetExceeded { .. } => Self::limit(request_id),
209            ExactRetrievalError::EmptyCandidateKey
210            | ExactRetrievalError::DuplicateCandidateKey
211            | ExactRetrievalError::DimensionMismatch { .. }
212            | ExactRetrievalError::ZeroLimit
213            | ExactRetrievalError::InvalidMinimumScore
214            | ExactRetrievalError::InvalidMinimumMargin => Self::invalid(request_id),
215            ExactRetrievalError::ArithmeticOverflow => Self::internal(request_id),
216        }
217    }
218
219    fn from_query(error: &QueryError, request_id: &str) -> Self {
220        match error {
221            QueryError::TimedOut => Self::timeout(request_id),
222            QueryError::ResultLimitExceeded { .. }
223            | QueryError::FilterNodesExceeded { .. }
224            | QueryError::FilterDepthExceeded { .. }
225            | QueryError::SortFieldsExceeded { .. }
226            | QueryError::GroupFieldsExceeded { .. }
227            | QueryError::MetricsExceeded { .. }
228            | QueryError::ScannedBudgetExceeded { .. }
229            | QueryError::MatchedBudgetExceeded { .. }
230            | QueryError::GroupBudgetExceeded { .. } => Self::limit(request_id),
231            QueryError::EmptyRecordKey
232            | QueryError::DuplicateRecordKey
233            | QueryError::ZeroLimit
234            | QueryError::CursorShape { .. }
235            | QueryError::EmptyCursorKey
236            | QueryError::NoncanonicalCursorNull
237            | QueryError::InvalidPrefixType
238            | QueryError::InvalidFieldPath
239            | QueryError::EmptyMetricName
240            | QueryError::DuplicateMetricName { .. }
241            | QueryError::MetricTypeMismatch { .. }
242            | QueryError::ArithmeticOverflow { .. }
243            | QueryError::MetricStateMismatch => Self::invalid(request_id),
244        }
245    }
246
247    fn from_storage(error: &StorageError, request_id: &str) -> Self {
248        match error {
249            StorageError::Index { source }
250                if matches!(
251                    source.as_ref(),
252                    MaterializedIndexError::VectorSpaceConflict { .. }
253                        | MaterializedIndexError::LexicalIndexConflict { .. }
254                ) =>
255            {
256                Self::new(
257                    StatusCode::CONFLICT,
258                    "definition_conflict",
259                    "immutable retrieval definition already exists with different contents",
260                    request_id,
261                )
262            }
263            StorageError::Index { source }
264                if matches!(
265                    source.as_ref(),
266                    MaterializedIndexError::UnknownVectorSpace { .. }
267                        | MaterializedIndexError::UnknownLexicalIndex { .. }
268                        | MaterializedIndexError::Vector(_)
269                ) =>
270            {
271                Self::invalid(request_id)
272            }
273            StorageError::Index { source } => {
274                if let MaterializedIndexError::Lexical(source) = source.as_ref() {
275                    return Self::from_lexical(source, request_id);
276                }
277                Self::internal(request_id)
278            }
279            StorageError::Mutation(
280                MutationError::EmptyKey
281                | MutationError::KeyTooLarge { .. }
282                | MutationError::OperationTooLarge { .. },
283            )
284            | StorageError::Log(
285                LogError::EmptyTransaction
286                | LogError::TooManyOperations
287                | LogError::PayloadTooLarge { .. },
288            ) => Self::limit(request_id),
289            StorageError::Log(LogError::Io(source))
290                if matches!(
291                    storage_limit_from_io(source),
292                    Some(StorageLimitError::TimedOut)
293                ) =>
294            {
295                Self::timeout(request_id)
296            }
297            StorageError::Log(LogError::Io(source)) if storage_limit_from_io(source).is_some() => {
298                Self::limit(request_id)
299            }
300            StorageError::Snapshot { source } if source.is_timeout() => Self::timeout(request_id),
301            StorageError::Snapshot { source } if source.storage_limit().is_some() => {
302                Self::limit(request_id)
303            }
304            StorageError::Snapshot { source }
305                if matches!(
306                    source.as_ref(),
307                    SnapshotError::FileLimitExceeded { .. }
308                        | SnapshotError::EntryLimitExceeded { .. }
309                        | SnapshotError::DecodedBytesLimitExceeded { .. }
310                ) =>
311            {
312                Self::result_too_large(request_id)
313            }
314            StorageError::Log(LogError::IdempotencyConflict { .. }) => Self::new(
315                StatusCode::CONFLICT,
316                "idempotency_conflict",
317                "transaction identifier was already committed with different contents",
318                request_id,
319            ),
320            _ => Self::internal(request_id),
321        }
322    }
323}
324
325impl IntoResponse for ApiError {
326    fn into_response(self) -> Response {
327        let envelope = ErrorV1 {
328            code: self.code.to_owned(),
329            message: self.message.to_owned(),
330            request_id: self.request_id,
331        };
332        let mut response = (self.status, Json(envelope)).into_response();
333        if self.status == StatusCode::UNAUTHORIZED {
334            response.headers_mut().insert(
335                header::WWW_AUTHENTICATE,
336                HeaderValue::from_static("Bearer realm=\"hyphae\""),
337            );
338        }
339        if self.status == StatusCode::TOO_MANY_REQUESTS {
340            response
341                .headers_mut()
342                .insert(header::RETRY_AFTER, HeaderValue::from_static("1"));
343        }
344        response
345    }
346}