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