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};
12use hyphae_query::QueryError;
13use hyphae_storage::{LogError, MutationError, StorageError};
14use thiserror::Error;
15
16use crate::ServerConfigError;
17
18#[derive(Debug, Error)]
20pub enum ServerError {
21 #[error(transparent)]
23 Configuration(#[from] ServerConfigError),
24 #[error("failed to open Hyphae engine: {0}")]
26 Engine(#[from] EngineError),
27 #[error("failed to bind Hyphae server at {address}: {source}")]
29 Bind {
30 address: SocketAddr,
32 #[source]
34 source: io::Error,
35 },
36 #[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}