1use 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#[derive(Debug, Error)]
24pub enum ServerError {
25 #[error(transparent)]
27 Configuration(#[from] ServerConfigError),
28 #[error("failed to open Hyphae engine: {0}")]
30 Engine(#[from] EngineError),
31 #[error("failed to bind Hyphae server at {address}: {source}")]
33 Bind {
34 address: SocketAddr,
36 #[source]
38 source: io::Error,
39 },
40 #[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}