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