Skip to main content

hyphae_server/
server.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::{
4    future::Future,
5    net::SocketAddr,
6    path::PathBuf,
7    pin::Pin,
8    sync::{
9        Arc, Mutex,
10        atomic::{AtomicBool, Ordering},
11    },
12    task::{Context, Poll},
13    time::Duration,
14};
15
16use axum::{
17    Router,
18    body::{self, Body},
19    extract::{Extension, Request, State},
20    http::{HeaderMap, HeaderValue, StatusCode, header},
21    middleware::{self, Next},
22    response::{IntoResponse, Response},
23    routing::{get, post},
24};
25use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
26use hyphae_contracts::v1::{
27    CapabilitiesV1, CommitReceiptV1, DefineLexicalIndexRequestV1, DefineVectorSpaceRequestV1,
28    DeleteRequestV1, DeleteVectorsRequestV1, ExactAbstentionReasonV1, ExactAbstentionV1,
29    ExactRetrievalMatchV1, ExactRetrievalOutcomeV1, ExactRetrievalRequestV1,
30    ExactRetrievalResponseV1, GetRequestV1, GetResponseV1, HealthV1, HybridAbstentionV1,
31    HybridBranchAbsenceV1, HybridExplanationV1, HybridRetrievalMatchV1, HybridRetrievalOutcomeV1,
32    HybridRetrievalRequestV1, HybridRetrievalResponseV1, LexicalAbstentionReasonV1,
33    LexicalAbstentionV1, LexicalFieldContributionV1, LexicalRetrievalMatchV1,
34    LexicalRetrievalOutcomeV1, LexicalRetrievalRequestV1, LexicalRetrievalResponseV1,
35    LexicalTermContributionV1, ProofV1, PutRequestV1, PutVectorsRequestV1, QueryRequestV1,
36    QueryResponseV1, RecordV1, RetrievalProofV1, VectorMetricV1, WitnessV1, decode_key_hex,
37    encode_hex,
38};
39use hyphae_core::{Q15Vector, VectorSpaceDefinition, VectorSpaceName, current_version};
40use hyphae_engine::{
41    BoundedEngineQueryError, EngineError, ExactRetrievalProofArtifact,
42    HybridRetrievalProofArtifact, HyphaeEngine, LexicalRetrievalProofArtifact, MaintenanceLimits,
43    ProofError, ProvenResult, ResultProofArtifact, StorageLimits,
44};
45use hyphae_query::{DEFAULT_QUERY_SCAN_BYTES, FieldPath};
46use hyphae_retrieval::{
47    ExactAbstentionReason, ExactRetrievalOutcome, ExactRetrievalRequest, HybridBranchAbsence,
48    HybridOutcome, HybridRequest, LexicalAbstentionReason, LexicalField, LexicalIndexDefinition,
49    LexicalOutcome, LexicalRequest,
50};
51use hyphae_storage::{
52    AppendOutcome, LogError, MaterializedIndexError, SnapshotError, SnapshotReadLimits,
53    StorageError, open_verified_snapshot_with_limits,
54};
55use serde::{Serialize, de::DeserializeOwned};
56use tokio::{
57    io::{AsyncRead, AsyncReadExt as _, ReadBuf},
58    net::TcpListener,
59    sync::{OwnedSemaphorePermit, Semaphore},
60};
61use tokio_util::io::ReaderStream;
62use uuid::Uuid;
63
64use crate::{ApiError, BearerToken, ServerConfig, ServerError, ServerLimits};
65
66const FEATURES: [&str; 14] = [
67    "atomic_batch",
68    "deterministic_query",
69    "durable_vectors",
70    "exact_retrieval",
71    "hybrid_retrieval",
72    "idempotency",
73    "kv",
74    "lexical_retrieval",
75    "offline_result_proof",
76    "offline_retrieval_proof",
77    "provider_free_lexical",
78    "snapshot_witness",
79    "structured_aggregation",
80    "typed_abstention",
81];
82const WITNESS_VERIFICATION_TIMEOUT: Duration = Duration::from_secs(60);
83
84#[derive(Clone, Debug)]
85struct RequestId(String);
86
87struct ServerState {
88    engine: Arc<Mutex<HyphaeEngine>>,
89    data_dir: PathBuf,
90    limits: ServerLimits,
91    bearer_token: Option<BearerToken>,
92    admission: Arc<Semaphore>,
93    ready: AtomicBool,
94}
95
96struct AdmittedFile {
97    file: tokio::fs::File,
98    _permit: OwnedSemaphorePermit,
99}
100
101impl AsyncRead for AdmittedFile {
102    fn poll_read(
103        self: Pin<&mut Self>,
104        context: &mut Context<'_>,
105        buffer: &mut ReadBuf<'_>,
106    ) -> Poll<std::io::Result<()>> {
107        Pin::new(&mut self.get_mut().file).poll_read(context, buffer)
108    }
109}
110
111/// Opened optional HTTP surface owning exactly one embedded Hyphae engine.
112pub struct HyphaeServer {
113    bind: SocketAddr,
114    state: Arc<ServerState>,
115}
116
117impl HyphaeServer {
118    /// Validates secure defaults and opens the exclusively owned engine.
119    ///
120    /// No socket is opened by this method. In particular, a non-loopback bind
121    /// without authentication fails here before [`Self::bind`].
122    ///
123    /// # Errors
124    ///
125    /// Returns a configuration, data-directory lock, recovery, or corruption
126    /// error.
127    pub fn open(config: ServerConfig) -> Result<Self, ServerError> {
128        Self::open_with_storage_limits(config, StorageLimits::default())
129    }
130
131    /// Validates secure defaults and opens the engine under explicit finite
132    /// storage recovery and maintenance limits.
133    ///
134    /// No socket is opened by this method.
135    ///
136    /// # Errors
137    ///
138    /// Returns a configuration, limit, timeout, data-directory lock,
139    /// recovery, or corruption error.
140    pub fn open_with_storage_limits(
141        config: ServerConfig,
142        storage_limits: StorageLimits,
143    ) -> Result<Self, ServerError> {
144        config.validate()?;
145        let opened = HyphaeEngine::open_with_limits(config.data_dir(), storage_limits)?;
146        let data_dir = opened.engine.data_path().to_path_buf();
147        Ok(Self {
148            bind: config.bind,
149            state: Arc::new(ServerState {
150                engine: Arc::new(Mutex::new(opened.engine)),
151                data_dir,
152                admission: Arc::new(Semaphore::new(config.limits.concurrent_operations)),
153                ready: AtomicBool::new(true),
154                limits: config.limits,
155                bearer_token: config.bearer_token,
156            }),
157        })
158    }
159
160    /// Opens the configured TCP listener and prepares graceful serving.
161    ///
162    /// # Errors
163    ///
164    /// Returns an operating-system socket bind failure.
165    pub async fn bind(self) -> Result<BoundServer, ServerError> {
166        let listener = TcpListener::bind(self.bind)
167            .await
168            .map_err(|source| ServerError::Bind {
169                address: self.bind,
170                source,
171            })?;
172        let local_addr = listener.local_addr().map_err(|source| ServerError::Bind {
173            address: self.bind,
174            source,
175        })?;
176        Ok(BoundServer {
177            listener,
178            local_addr,
179            router: build_router(self.state),
180        })
181    }
182
183    #[cfg(test)]
184    fn test_router(&self) -> Router {
185        build_router(Arc::clone(&self.state))
186    }
187}
188
189/// Successfully bound HTTP service awaiting a shutdown signal.
190pub struct BoundServer {
191    listener: TcpListener,
192    local_addr: SocketAddr,
193    router: Router,
194}
195
196impl BoundServer {
197    /// Returns the actual local address, including an assigned ephemeral port.
198    pub fn local_addr(&self) -> SocketAddr {
199        self.local_addr
200    }
201
202    /// Serves until the supplied graceful-shutdown future resolves.
203    ///
204    /// # Errors
205    ///
206    /// Returns an HTTP listener/service I/O failure.
207    pub async fn run_with_shutdown<F>(self, shutdown: F) -> Result<(), ServerError>
208    where
209        F: Future<Output = ()> + Send + 'static,
210    {
211        axum::serve(self.listener, self.router)
212            .with_graceful_shutdown(shutdown)
213            .await
214            .map_err(ServerError::Serve)
215    }
216}
217
218fn build_router(state: Arc<ServerState>) -> Router {
219    let public = Router::new()
220        .route("/v1/capabilities", get(capabilities))
221        .route("/v1/health/live", get(liveness))
222        .route("/v1/health/ready", get(readiness));
223    let protected = Router::new()
224        .route("/v1/kv/put", post(put_records))
225        .route("/v1/kv/get", post(get_record))
226        .route("/v1/kv/delete", post(delete_records))
227        .route("/v1/query", post(query_records))
228        .route("/v1/vector-spaces/define", post(define_vector_space))
229        .route("/v1/vectors/put", post(put_vectors))
230        .route("/v1/vectors/delete", post(delete_vectors))
231        .route("/v1/retrieve/exact", post(retrieve_exact))
232        .route("/v1/lexical-indexes/define", post(define_lexical_index))
233        .route("/v1/retrieve/lexical", post(retrieve_lexical))
234        .route("/v1/retrieve/hybrid", post(retrieve_hybrid))
235        .route(
236            "/v1/witnesses/{checkpoint_sequence}/{snapshot_digest}",
237            get(download_witness),
238        )
239        .route_layer(middleware::from_fn_with_state(
240            Arc::clone(&state),
241            authenticate,
242        ));
243
244    public
245        .merge(protected)
246        .fallback(route_not_found)
247        .method_not_allowed_fallback(method_not_allowed)
248        .with_state(state)
249        .layer(middleware::from_fn(assign_request_id))
250}
251
252async fn assign_request_id(mut request: Request, next: Next) -> Response {
253    let request_id = RequestId(Uuid::now_v7().to_string());
254    request.extensions_mut().insert(request_id.clone());
255    let mut response = next.run(request).await;
256    if let Ok(value) = HeaderValue::from_str(&request_id.0) {
257        response.headers_mut().insert("x-request-id", value);
258    }
259    response
260}
261
262async fn authenticate(
263    State(state): State<Arc<ServerState>>,
264    request: Request,
265    next: Next,
266) -> Response {
267    let Some(expected) = &state.bearer_token else {
268        return next.run(request).await;
269    };
270    let request_id = request_id(&request);
271    if bearer_candidate(request.headers()).is_some_and(|candidate| expected.verifies(candidate)) {
272        return next.run(request).await;
273    }
274    ApiError::new(
275        StatusCode::UNAUTHORIZED,
276        "unauthorized",
277        "valid bearer authentication is required",
278        request_id,
279    )
280    .into_response()
281}
282
283fn bearer_candidate(headers: &HeaderMap) -> Option<&[u8]> {
284    let mut values = headers.get_all(header::AUTHORIZATION).iter();
285    let value = values.next()?;
286    if values.next().is_some() {
287        return None;
288    }
289    let value = value.as_bytes();
290    let separator = value.iter().position(|byte| *byte == b' ')?;
291    if !value[..separator].eq_ignore_ascii_case(b"bearer") {
292        return None;
293    }
294    let candidate = &value[separator.saturating_add(1)..];
295    (!candidate.is_empty()).then_some(candidate)
296}
297
298async fn liveness(
299    State(state): State<Arc<ServerState>>,
300    Extension(request_id): Extension<RequestId>,
301) -> Result<Response, ApiError> {
302    bounded_json(
303        &HealthV1 {
304            status: "live".to_owned(),
305        },
306        &state,
307        &request_id.0,
308    )
309}
310
311async fn readiness(
312    State(state): State<Arc<ServerState>>,
313    Extension(request_id): Extension<RequestId>,
314) -> Result<Response, ApiError> {
315    if !state.ready.load(Ordering::Acquire) {
316        return Err(ApiError::unavailable(&request_id.0));
317    }
318    bounded_json(
319        &HealthV1 {
320            status: "ready".to_owned(),
321        },
322        &state,
323        &request_id.0,
324    )
325}
326
327async fn capabilities(
328    State(state): State<Arc<ServerState>>,
329    Extension(request_id): Extension<RequestId>,
330) -> Result<Response, ApiError> {
331    let version = current_version();
332    bounded_json(
333        &CapabilitiesV1 {
334            api_version: version.api.to_owned(),
335            disk_format_version: version.disk_format,
336            features: FEATURES.iter().map(ToString::to_string).collect(),
337            limits: state.limits.as_contract(),
338        },
339        &state,
340        &request_id.0,
341    )
342}
343
344async fn put_records(
345    State(state): State<Arc<ServerState>>,
346    Extension(request_id): Extension<RequestId>,
347    request: Request,
348) -> Result<Response, ApiError> {
349    let request: PutRequestV1 = parse_json(request, &state, &request_id.0).await?;
350    validate_batch(request.records.len(), &state, &request_id.0)?;
351    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
352    let records = request
353        .records
354        .iter()
355        .map(RecordV1::to_domain)
356        .collect::<Result<Vec<_>, _>>()
357        .map_err(|_| ApiError::invalid(&request_id.0))?;
358    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
359        capture_write_outcome(engine.put_records(transaction_id, &records))
360    })
361    .await?;
362    if outcome.requires_recovery {
363        state.ready.store(false, Ordering::Release);
364    }
365    bounded_json(&receipt(outcome.append), &state, &request_id.0)
366}
367
368async fn delete_records(
369    State(state): State<Arc<ServerState>>,
370    Extension(request_id): Extension<RequestId>,
371    request: Request,
372) -> Result<Response, ApiError> {
373    let request: DeleteRequestV1 = parse_json(request, &state, &request_id.0).await?;
374    validate_batch(request.keys_hex.len(), &state, &request_id.0)?;
375    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
376    let keys = request
377        .keys_hex
378        .iter()
379        .map(|key| decode_key_hex(key))
380        .collect::<Result<Vec<_>, _>>()
381        .map_err(|_| ApiError::invalid(&request_id.0))?;
382    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
383        let keys = keys.iter().map(Vec::as_slice).collect::<Vec<_>>();
384        capture_write_outcome(engine.delete_records(transaction_id, &keys))
385    })
386    .await?;
387    if outcome.requires_recovery {
388        state.ready.store(false, Ordering::Release);
389    }
390    bounded_json(&receipt(outcome.append), &state, &request_id.0)
391}
392
393async fn get_record(
394    State(state): State<Arc<ServerState>>,
395    Extension(request_id): Extension<RequestId>,
396    request: Request,
397) -> Result<Response, ApiError> {
398    let request: GetRequestV1 = parse_json(request, &state, &request_id.0).await?;
399    let key = decode_key_hex(&request.key_hex).map_err(|_| ApiError::invalid(&request_id.0))?;
400    let maintenance =
401        witness_maintenance_limits(state.limits.witness_bytes, WITNESS_VERIFICATION_TIMEOUT);
402    let artifact = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
403        engine.get_record_with_proof_with_limits(&key, &maintenance)
404    })
405    .await?;
406    let proof = proof_transport(&artifact, &state, &request_id.0)?;
407    let ProvenResult::Get(record) = artifact.proof.result() else {
408        return Err(ApiError::internal(&request_id.0));
409    };
410    let response = GetResponseV1 {
411        found: record.is_some(),
412        record: record.as_ref().map(RecordV1::from_domain),
413        proof,
414    };
415    bounded_json(&response, &state, &request_id.0)
416}
417
418async fn query_records(
419    State(state): State<Arc<ServerState>>,
420    Extension(request_id): Extension<RequestId>,
421    request: Request,
422) -> Result<Response, ApiError> {
423    let request: QueryRequestV1 = parse_json(request, &state, &request_id.0).await?;
424    let timeout = requested_timeout(request.timeout_ms, &state, &request_id.0)?;
425    let query = request
426        .to_domain()
427        .map_err(|_| ApiError::invalid(&request_id.0))?;
428    let mut execution_limits = state.limits.query.clone();
429    execution_limits.timeout = timeout;
430    let maintenance = witness_maintenance_limits(state.limits.witness_bytes, timeout);
431    let artifact = with_bounded_query(Arc::clone(&state), &request_id.0, move |engine| {
432        engine.query_with_proof_with_limits(
433            &query,
434            &execution_limits,
435            DEFAULT_QUERY_SCAN_BYTES,
436            &maintenance,
437        )
438    })
439    .await?;
440    let proof = proof_transport(&artifact, &state, &request_id.0)?;
441    let ProvenResult::Query(result) = artifact.proof.result() else {
442        return Err(ApiError::internal(&request_id.0));
443    };
444    bounded_json(
445        &QueryResponseV1::from_domain(result, proof),
446        &state,
447        &request_id.0,
448    )
449}
450
451async fn define_vector_space(
452    State(state): State<Arc<ServerState>>,
453    Extension(request_id): Extension<RequestId>,
454    request: Request,
455) -> Result<Response, ApiError> {
456    let request: DefineVectorSpaceRequestV1 = parse_json(request, &state, &request_id.0).await?;
457    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
458    if request.vector_space.metric != VectorMetricV1::CosineQ15Nanos {
459        return Err(ApiError::invalid(&request_id.0));
460    }
461    let name = VectorSpaceName::new(request.vector_space.name)
462        .map_err(|_| ApiError::invalid(&request_id.0))?;
463    let definition = VectorSpaceDefinition::cosine(name, request.vector_space.dimension)
464        .map_err(|_| ApiError::invalid(&request_id.0))?;
465    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
466        capture_write_outcome(engine.define_vector_space(transaction_id, definition))
467    })
468    .await?;
469    if outcome.requires_recovery {
470        state.ready.store(false, Ordering::Release);
471    }
472    bounded_json(&receipt(outcome.append), &state, &request_id.0)
473}
474
475async fn put_vectors(
476    State(state): State<Arc<ServerState>>,
477    Extension(request_id): Extension<RequestId>,
478    request: Request,
479) -> Result<Response, ApiError> {
480    let request: PutVectorsRequestV1 = parse_json(request, &state, &request_id.0).await?;
481    validate_batch(request.vectors.len(), &state, &request_id.0)?;
482    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
483    let space =
484        VectorSpaceName::new(request.vector_space).map_err(|_| ApiError::invalid(&request_id.0))?;
485    let vectors = request
486        .vectors
487        .into_iter()
488        .map(|vector| {
489            Ok((
490                decode_key_hex(&vector.key_hex).map_err(|_| ApiError::invalid(&request_id.0))?,
491                Q15Vector::new(vector.values).map_err(|_| ApiError::invalid(&request_id.0))?,
492            ))
493        })
494        .collect::<Result<Vec<_>, ApiError>>()?;
495    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
496        capture_write_outcome(engine.put_vectors(transaction_id, &space, &vectors))
497    })
498    .await?;
499    if outcome.requires_recovery {
500        state.ready.store(false, Ordering::Release);
501    }
502    bounded_json(&receipt(outcome.append), &state, &request_id.0)
503}
504
505async fn delete_vectors(
506    State(state): State<Arc<ServerState>>,
507    Extension(request_id): Extension<RequestId>,
508    request: Request,
509) -> Result<Response, ApiError> {
510    let request: DeleteVectorsRequestV1 = parse_json(request, &state, &request_id.0).await?;
511    validate_batch(request.keys_hex.len(), &state, &request_id.0)?;
512    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
513    let space =
514        VectorSpaceName::new(request.vector_space).map_err(|_| ApiError::invalid(&request_id.0))?;
515    let keys = request
516        .keys_hex
517        .iter()
518        .map(|key| decode_key_hex(key))
519        .collect::<Result<Vec<_>, _>>()
520        .map_err(|_| ApiError::invalid(&request_id.0))?;
521    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
522        let keys = keys.iter().map(Vec::as_slice).collect::<Vec<_>>();
523        capture_write_outcome(engine.delete_vectors(transaction_id, &space, &keys))
524    })
525    .await?;
526    if outcome.requires_recovery {
527        state.ready.store(false, Ordering::Release);
528    }
529    bounded_json(&receipt(outcome.append), &state, &request_id.0)
530}
531
532async fn retrieve_exact(
533    State(state): State<Arc<ServerState>>,
534    Extension(request_id): Extension<RequestId>,
535    request: Request,
536) -> Result<Response, ApiError> {
537    let request: ExactRetrievalRequestV1 = parse_json(request, &state, &request_id.0).await?;
538    let timeout = requested_retrieval_timeout(request.timeout_ms, &state, &request_id.0)?;
539    let request = exact_request(request, &request_id.0)?;
540    let mut limits = state.limits.exact_retrieval.clone();
541    limits.timeout = timeout;
542    let maintenance = witness_maintenance_limits(state.limits.witness_bytes, timeout);
543    let artifact = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
544        engine.retrieve_exact_with_proof_with_limits(&request, &limits, &maintenance)
545    })
546    .await?;
547    let proof = retrieval_proof_transport(&artifact, &state, &request_id.0)?;
548    let response = ExactRetrievalResponseV1 {
549        outcome: exact_outcome_transport(artifact.proof.outcome()),
550        proof,
551    };
552    bounded_json(&response, &state, &request_id.0)
553}
554
555fn exact_request(
556    request: ExactRetrievalRequestV1,
557    request_id: &str,
558) -> Result<ExactRetrievalRequest, ApiError> {
559    Ok(ExactRetrievalRequest {
560        vector_space: VectorSpaceName::new(request.vector_space)
561            .map_err(|_| ApiError::invalid(request_id))?,
562        query: Q15Vector::new(request.query).map_err(|_| ApiError::invalid(request_id))?,
563        limit: usize::try_from(request.limit).map_err(|_| ApiError::limit(request_id))?,
564        minimum_score_nanos: request.minimum_score_nanos,
565        minimum_margin_nanos: request.minimum_margin_nanos,
566    })
567}
568
569fn lexical_request(
570    request: LexicalRetrievalRequestV1,
571    request_id: &str,
572) -> Result<LexicalRequest, ApiError> {
573    Ok(LexicalRequest {
574        index: VectorSpaceName::new(request.lexical_index)
575            .map_err(|_| ApiError::invalid(request_id))?,
576        query: request.query,
577        limit: usize::try_from(request.limit).map_err(|_| ApiError::limit(request_id))?,
578    })
579}
580
581async fn define_lexical_index(
582    State(state): State<Arc<ServerState>>,
583    Extension(request_id): Extension<RequestId>,
584    request: Request,
585) -> Result<Response, ApiError> {
586    let request: DefineLexicalIndexRequestV1 = parse_json(request, &state, &request_id.0).await?;
587    let transaction_id = parse_transaction_id(request.transaction_id.as_deref(), &request_id.0)?;
588    let name = VectorSpaceName::new(request.lexical_index.name)
589        .map_err(|_| ApiError::invalid(&request_id.0))?;
590    let fields = request
591        .lexical_index
592        .fields
593        .into_iter()
594        .map(|field| {
595            Ok(LexicalField {
596                path: FieldPath::new(field.path),
597                weight_micros: field.weight_micros,
598            })
599        })
600        .collect::<Result<Vec<_>, ApiError>>()?;
601    let definition =
602        LexicalIndexDefinition::new(name, fields).map_err(|_| ApiError::invalid(&request_id.0))?;
603    let outcome = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
604        capture_write_outcome(engine.define_lexical_index(transaction_id, definition))
605    })
606    .await?;
607    if outcome.requires_recovery {
608        state.ready.store(false, Ordering::Release);
609    }
610    bounded_json(&receipt(outcome.append), &state, &request_id.0)
611}
612
613async fn retrieve_lexical(
614    State(state): State<Arc<ServerState>>,
615    Extension(request_id): Extension<RequestId>,
616    request: Request,
617) -> Result<Response, ApiError> {
618    let request: LexicalRetrievalRequestV1 = parse_json(request, &state, &request_id.0).await?;
619    let timeout = requested_lexical_timeout(request.timeout_ms, &state, &request_id.0)?;
620    let request = lexical_request(request, &request_id.0)?;
621    let mut limits = state.limits.lexical_retrieval.clone();
622    limits.timeout = timeout;
623    let maintenance = witness_maintenance_limits(state.limits.witness_bytes, timeout);
624    let artifact = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
625        engine.retrieve_lexical_with_proof_with_limits(&request, &limits, &maintenance)
626    })
627    .await?;
628    let proof = lexical_retrieval_proof_transport(&artifact, &state, &request_id.0)?;
629    bounded_json(
630        &LexicalRetrievalResponseV1 {
631            outcome: lexical_outcome_transport(artifact.proof.outcome()),
632            proof,
633        },
634        &state,
635        &request_id.0,
636    )
637}
638
639async fn retrieve_hybrid(
640    State(state): State<Arc<ServerState>>,
641    Extension(request_id): Extension<RequestId>,
642    request: Request,
643) -> Result<Response, ApiError> {
644    let request: HybridRetrievalRequestV1 = parse_json(request, &state, &request_id.0).await?;
645    let lexical_timeout =
646        requested_lexical_timeout(request.lexical.timeout_ms, &state, &request_id.0)?;
647    let vector_timeout =
648        requested_retrieval_timeout(request.vector.timeout_ms, &state, &request_id.0)?;
649    let lexical_request = lexical_request(request.lexical, &request_id.0)?;
650    let vector_request = exact_request(request.vector, &request_id.0)?;
651    let hybrid_request = HybridRequest {
652        lexical_weight: request.lexical_weight,
653        vector_weight: request.vector_weight,
654        limit: usize::try_from(request.limit).map_err(|_| ApiError::limit(&request_id.0))?,
655    };
656    if hybrid_request.limit > state.limits.lexical_retrieval.max_returned {
657        return Err(ApiError::limit(&request_id.0));
658    }
659    let mut lexical_limits = state.limits.lexical_retrieval.clone();
660    lexical_limits.timeout = lexical_timeout;
661    let mut vector_limits = state.limits.exact_retrieval.clone();
662    vector_limits.timeout = vector_timeout;
663    let maintenance = witness_maintenance_limits(
664        state.limits.witness_bytes,
665        lexical_timeout
666            .checked_add(vector_timeout)
667            .unwrap_or(Duration::MAX),
668    );
669    let artifact = with_engine(Arc::clone(&state), &request_id.0, move |engine| {
670        engine.retrieve_hybrid_with_proof_with_limits(
671            &lexical_request,
672            &lexical_limits,
673            &vector_request,
674            &vector_limits,
675            &hybrid_request,
676            &maintenance,
677        )
678    })
679    .await?;
680    let proof = hybrid_retrieval_proof_transport(&artifact, &state, &request_id.0)?;
681    bounded_json(
682        &HybridRetrievalResponseV1 {
683            outcome: hybrid_outcome_transport(artifact.proof.outcome()),
684            proof,
685        },
686        &state,
687        &request_id.0,
688    )
689}
690
691async fn download_witness(
692    State(state): State<Arc<ServerState>>,
693    Extension(request_id): Extension<RequestId>,
694    request: Request,
695) -> Result<Response, ApiError> {
696    let (sequence, expected_digest) =
697        parse_witness_path(request.uri().path()).ok_or_else(|| ApiError::invalid(&request_id.0))?;
698    let path = state
699        .data_dir
700        .join("snapshots")
701        .join(format!("snapshot-{sequence:020}.hysnap"));
702    let permit = Arc::clone(&state.admission)
703        .try_acquire_owned()
704        .map_err(|_| busy(&request_id.0))?;
705    let verification_path = path.clone();
706    let verification_limits = SnapshotReadLimits {
707        file_bytes: state.limits.witness_bytes,
708        entries: state.limits.witness_bytes,
709        decoded_bytes: state.limits.witness_bytes,
710    };
711    let verified = tokio::task::spawn_blocking(move || {
712        open_verified_snapshot_with_limits(
713            verification_path,
714            &verification_limits,
715            WITNESS_VERIFICATION_TIMEOUT,
716        )
717    })
718    .await;
719    let (verified_file, info) = match verified {
720        Ok(Ok(verified)) => verified,
721        Ok(Err(SnapshotError::Io(source))) if source.kind() == std::io::ErrorKind::NotFound => {
722            return Err(not_found(&request_id.0));
723        }
724        Ok(Err(
725            SnapshotError::FileLimitExceeded { .. }
726            | SnapshotError::EntryLimitExceeded { .. }
727            | SnapshotError::DecodedBytesLimitExceeded { .. },
728        )) => {
729            return Err(ApiError::result_too_large(&request_id.0));
730        }
731        Ok(Err(source)) if source.is_timeout() => {
732            return Err(ApiError::timeout(&request_id.0));
733        }
734        Ok(Err(_)) | Err(_) => return Err(ApiError::internal(&request_id.0)),
735    };
736    if info.checkpoint_sequence != sequence || info.snapshot_digest != expected_digest {
737        return Err(not_found(&request_id.0));
738    }
739    let stream = ReaderStream::new(
740        AdmittedFile {
741            file: tokio::fs::File::from_std(verified_file),
742            _permit: permit,
743        }
744        .take(info.file_bytes),
745    );
746    Response::builder()
747        .status(StatusCode::OK)
748        .header(header::CONTENT_TYPE, "application/octet-stream")
749        .header(header::CONTENT_LENGTH, info.file_bytes)
750        .header(
751            "digest",
752            format!("blake3={}", encode_hex(&info.snapshot_digest)),
753        )
754        .body(Body::from_stream(stream))
755        .map_err(|_| ApiError::internal(&request_id.0))
756}
757
758async fn parse_json<T: DeserializeOwned>(
759    request: Request,
760    state: &ServerState,
761    request_id: &str,
762) -> Result<T, ApiError> {
763    if !is_json_content_type(request.headers()) {
764        return Err(ApiError::new(
765            StatusCode::UNSUPPORTED_MEDIA_TYPE,
766            "unsupported_media_type",
767            "content type must be application/json",
768            request_id,
769        ));
770    }
771    let bytes = tokio::time::timeout(
772        state.limits.request_body_timeout,
773        body::to_bytes(request.into_body(), state.limits.request_body_bytes),
774    )
775    .await
776    .map_err(|_| {
777        ApiError::new(
778            StatusCode::REQUEST_TIMEOUT,
779            "timeout",
780            "request body deadline elapsed without starting an operation",
781            request_id,
782        )
783    })?
784    .map_err(|_| ApiError::payload_too_large(request_id))?;
785    if bytes.is_empty() {
786        return Err(ApiError::invalid(request_id));
787    }
788    let value: serde_json::Value =
789        serde_json::from_slice(&bytes).map_err(|_| ApiError::invalid(request_id))?;
790    validate_json_shape(&value, state.limits.json_depth, state.limits.json_nodes)
791        .map_err(|()| ApiError::limit(request_id))?;
792    serde_json::from_value(value).map_err(|_| ApiError::invalid(request_id))
793}
794
795fn validate_json_shape(
796    root: &serde_json::Value,
797    maximum_depth: usize,
798    maximum_nodes: usize,
799) -> Result<(), ()> {
800    let mut stack = vec![(root, 0_usize)];
801    let mut nodes = 0_usize;
802    while let Some((value, depth)) = stack.pop() {
803        nodes = nodes.checked_add(1).ok_or(())?;
804        if nodes > maximum_nodes || depth > maximum_depth {
805            return Err(());
806        }
807        match value {
808            serde_json::Value::Array(values) => {
809                let next_depth = depth.checked_add(1).ok_or(())?;
810                stack.extend(values.iter().map(|value| (value, next_depth)));
811            }
812            serde_json::Value::Object(values) => {
813                let next_depth = depth.checked_add(1).ok_or(())?;
814                stack.extend(values.values().map(|value| (value, next_depth)));
815            }
816            serde_json::Value::Null
817            | serde_json::Value::Bool(_)
818            | serde_json::Value::Number(_)
819            | serde_json::Value::String(_) => {}
820        }
821    }
822    Ok(())
823}
824
825fn is_json_content_type(headers: &HeaderMap) -> bool {
826    let Some(value) = headers.get(header::CONTENT_TYPE) else {
827        return false;
828    };
829    let Ok(value) = value.to_str() else {
830        return false;
831    };
832    let media_type = value.split(';').next().unwrap_or_default().trim();
833    let media_type = media_type.to_ascii_lowercase();
834    media_type == "application/json"
835        || (media_type.starts_with("application/") && media_type.ends_with("+json"))
836}
837
838fn validate_batch(length: usize, state: &ServerState, request_id: &str) -> Result<(), ApiError> {
839    if length == 0 {
840        return Err(ApiError::invalid(request_id));
841    }
842    if length > state.limits.batch_items {
843        return Err(ApiError::limit(request_id));
844    }
845    Ok(())
846}
847
848fn parse_transaction_id(value: Option<&str>, request_id: &str) -> Result<Uuid, ApiError> {
849    value.map_or_else(
850        || Ok(Uuid::now_v7()),
851        |value| Uuid::parse_str(value).map_err(|_| ApiError::invalid(request_id)),
852    )
853}
854
855fn requested_timeout(
856    requested_ms: Option<u64>,
857    state: &ServerState,
858    request_id: &str,
859) -> Result<Duration, ApiError> {
860    let maximum_ms = u64::try_from(state.limits.query.timeout.as_millis()).unwrap_or(u64::MAX);
861    let requested_ms = requested_ms.unwrap_or(maximum_ms);
862    if requested_ms == 0 {
863        return Err(ApiError::invalid(request_id));
864    }
865    if requested_ms > maximum_ms {
866        return Err(ApiError::limit(request_id));
867    }
868    Ok(Duration::from_millis(requested_ms))
869}
870
871fn requested_retrieval_timeout(
872    requested_ms: Option<u64>,
873    state: &ServerState,
874    request_id: &str,
875) -> Result<Duration, ApiError> {
876    let maximum_ms =
877        u64::try_from(state.limits.exact_retrieval.timeout.as_millis()).unwrap_or(u64::MAX);
878    let requested_ms = requested_ms.unwrap_or(maximum_ms);
879    if requested_ms == 0 {
880        return Err(ApiError::invalid(request_id));
881    }
882    if requested_ms > maximum_ms {
883        return Err(ApiError::limit(request_id));
884    }
885    Ok(Duration::from_millis(requested_ms))
886}
887
888fn requested_lexical_timeout(
889    requested_ms: Option<u64>,
890    state: &ServerState,
891    request_id: &str,
892) -> Result<Duration, ApiError> {
893    let maximum_ms =
894        u64::try_from(state.limits.lexical_retrieval.timeout.as_millis()).unwrap_or(u64::MAX);
895    let requested_ms = requested_ms.unwrap_or(maximum_ms);
896    if requested_ms == 0 {
897        return Err(ApiError::invalid(request_id));
898    }
899    if requested_ms > maximum_ms {
900        return Err(ApiError::limit(request_id));
901    }
902    Ok(Duration::from_millis(requested_ms))
903}
904
905fn witness_maintenance_limits(witness_bytes: u64, timeout: Duration) -> MaintenanceLimits {
906    MaintenanceLimits {
907        timeout: WITNESS_VERIFICATION_TIMEOUT.min(timeout),
908        snapshot: SnapshotReadLimits {
909            file_bytes: witness_bytes,
910            entries: witness_bytes,
911            decoded_bytes: witness_bytes,
912        },
913    }
914}
915
916async fn with_engine<T, F>(
917    state: Arc<ServerState>,
918    request_id: &str,
919    operation: F,
920) -> Result<T, ApiError>
921where
922    T: Send + 'static,
923    F: FnOnce(&mut HyphaeEngine) -> Result<T, EngineError> + Send + 'static,
924{
925    if !state.ready.load(Ordering::Acquire) {
926        return Err(ApiError::unavailable(request_id));
927    }
928    let _permit = Arc::clone(&state.admission)
929        .try_acquire_owned()
930        .map_err(|_| busy(request_id))?;
931    let engine = Arc::clone(&state.engine);
932    let result = tokio::task::spawn_blocking(move || {
933        let mut engine = engine.lock().map_err(|_| EngineTaskError::Poisoned)?;
934        operation(&mut engine).map_err(EngineTaskError::Engine)
935    })
936    .await;
937    match result {
938        Ok(Ok(value)) => Ok(value),
939        Ok(Err(EngineTaskError::Engine(source))) => {
940            if engine_error_requires_recovery(&source) {
941                state.ready.store(false, Ordering::Release);
942            }
943            Err(ApiError::from_engine(source, request_id))
944        }
945        Ok(Err(EngineTaskError::Poisoned)) | Err(_) => {
946            state.ready.store(false, Ordering::Release);
947            Err(ApiError::internal(request_id))
948        }
949    }
950}
951
952async fn with_bounded_query<T, F>(
953    state: Arc<ServerState>,
954    request_id: &str,
955    operation: F,
956) -> Result<T, ApiError>
957where
958    T: Send + 'static,
959    F: FnOnce(&mut HyphaeEngine) -> Result<T, BoundedEngineQueryError> + Send + 'static,
960{
961    if !state.ready.load(Ordering::Acquire) {
962        return Err(ApiError::unavailable(request_id));
963    }
964    let _permit = Arc::clone(&state.admission)
965        .try_acquire_owned()
966        .map_err(|_| busy(request_id))?;
967    let engine = Arc::clone(&state.engine);
968    let result = tokio::task::spawn_blocking(move || {
969        let mut engine = engine.lock().map_err(|_| BoundedQueryTaskError::Poisoned)?;
970        operation(&mut engine).map_err(BoundedQueryTaskError::Query)
971    })
972    .await;
973    match result {
974        Ok(Ok(value)) => Ok(value),
975        Ok(Err(BoundedQueryTaskError::Query(source))) => {
976            if let BoundedEngineQueryError::Engine(engine) = &source
977                && engine_error_requires_recovery(engine)
978            {
979                state.ready.store(false, Ordering::Release);
980            }
981            Err(ApiError::from_bounded_query(source, request_id))
982        }
983        Ok(Err(BoundedQueryTaskError::Poisoned)) | Err(_) => {
984            state.ready.store(false, Ordering::Release);
985            Err(ApiError::internal(request_id))
986        }
987    }
988}
989
990enum EngineTaskError {
991    Engine(EngineError),
992    Poisoned,
993}
994
995enum BoundedQueryTaskError {
996    Query(BoundedEngineQueryError),
997    Poisoned,
998}
999
1000struct WriteOutcome {
1001    append: AppendOutcome,
1002    requires_recovery: bool,
1003}
1004
1005fn capture_write_outcome(
1006    result: Result<AppendOutcome, EngineError>,
1007) -> Result<WriteOutcome, EngineError> {
1008    match result {
1009        Ok(append) => Ok(WriteOutcome {
1010            append,
1011            requires_recovery: false,
1012        }),
1013        Err(EngineError::Storage(StorageError::CommittedButNotIndexed { receipt, .. })) => {
1014            Ok(WriteOutcome {
1015                append: AppendOutcome::Committed(receipt),
1016                requires_recovery: true,
1017            })
1018        }
1019        Err(source) => Err(source),
1020    }
1021}
1022
1023fn engine_error_requires_recovery(error: &EngineError) -> bool {
1024    if matches!(
1025        error,
1026        EngineError::Proof(ProofError::ProofLimitExceeded { .. } | ProofError::LengthOverflow)
1027    ) {
1028        return false;
1029    }
1030    match error {
1031        EngineError::Storage(StorageError::Index { source }) => {
1032            materialized_index_error_requires_recovery(source)
1033        }
1034        EngineError::Storage(StorageError::Snapshot { source }) => {
1035            snapshot_error_requires_recovery(source)
1036        }
1037        EngineError::Storage(
1038            StorageError::CommittedButNotIndexed { .. }
1039            | StorageError::StaleIndex
1040            | StorageError::DataDirectory(_)
1041            | StorageError::Log(LogError::Poisoned),
1042        )
1043        | EngineError::Proof(_) => true,
1044        _ => false,
1045    }
1046}
1047
1048fn snapshot_error_requires_recovery(error: &SnapshotError) -> bool {
1049    if error.is_timeout() || error.storage_limit().is_some() {
1050        return false;
1051    }
1052    match error {
1053        SnapshotError::FileLimitExceeded { .. }
1054        | SnapshotError::EntryLimitExceeded { .. }
1055        | SnapshotError::DecodedBytesLimitExceeded { .. } => false,
1056        SnapshotError::Index { source } => materialized_index_error_requires_recovery(source),
1057        _ => true,
1058    }
1059}
1060
1061fn materialized_index_error_requires_recovery(error: &MaterializedIndexError) -> bool {
1062    !matches!(
1063        error,
1064        MaterializedIndexError::Vector(_)
1065            | MaterializedIndexError::UnknownVectorSpace { .. }
1066            | MaterializedIndexError::VectorSpaceConflict { .. }
1067            | MaterializedIndexError::Lexical(_)
1068            | MaterializedIndexError::LexicalIndexConflict { .. }
1069            | MaterializedIndexError::UnknownLexicalIndex { .. }
1070            | MaterializedIndexError::VectorCandidateBudgetExceeded { .. }
1071            | MaterializedIndexError::VectorByteBudgetExceeded { .. }
1072    )
1073}
1074
1075fn proof_transport(
1076    artifact: &ResultProofArtifact,
1077    state: &ServerState,
1078    request_id: &str,
1079) -> Result<ProofV1, ApiError> {
1080    let encoded = artifact
1081        .proof
1082        .to_bytes()
1083        .map_err(|source| ApiError::from_engine(EngineError::Proof(source), request_id))?;
1084    if encoded.len() > state.limits.proof_bytes
1085        || artifact.snapshot.file_bytes > state.limits.witness_bytes
1086    {
1087        return Err(ApiError::result_too_large(request_id));
1088    }
1089    let anchor = artifact.proof.anchor();
1090    let snapshot_digest = encode_hex(&anchor.snapshot_digest);
1091    Ok(ProofV1 {
1092        encoding: "base64".to_owned(),
1093        data: BASE64.encode(encoded),
1094        proof_digest: encode_hex(&artifact.proof.proof_digest()),
1095        anchor_digest: encode_hex(&artifact.proof.anchor_digest()),
1096        checkpoint_sequence: anchor.checkpoint_sequence,
1097        checkpoint_digest: anchor
1098            .checkpoint_digest
1099            .as_ref()
1100            .map(|digest| encode_hex(digest)),
1101        snapshot_digest: snapshot_digest.clone(),
1102        witness: WitnessV1 {
1103            path: format!(
1104                "/v1/witnesses/{}/{}",
1105                anchor.checkpoint_sequence, snapshot_digest
1106            ),
1107            file_bytes: artifact.snapshot.file_bytes,
1108        },
1109    })
1110}
1111
1112fn retrieval_proof_transport(
1113    artifact: &ExactRetrievalProofArtifact,
1114    state: &ServerState,
1115    request_id: &str,
1116) -> Result<RetrievalProofV1, ApiError> {
1117    let encoded = artifact
1118        .proof
1119        .to_bytes()
1120        .map_err(|source| ApiError::from_engine(EngineError::RetrievalProof(source), request_id))?;
1121    if encoded.len() > state.limits.proof_bytes
1122        || artifact.snapshot.file_bytes > state.limits.witness_bytes
1123    {
1124        return Err(ApiError::result_too_large(request_id));
1125    }
1126    let anchor = artifact.proof.anchor();
1127    let snapshot_digest = encode_hex(&anchor.snapshot_digest);
1128    Ok(RetrievalProofV1 {
1129        encoding: "base64".to_owned(),
1130        data: BASE64.encode(encoded),
1131        proof_digest: encode_hex(&artifact.proof.proof_digest()),
1132        anchor_digest: encode_hex(&artifact.proof.anchor_digest()),
1133        checkpoint_sequence: anchor.checkpoint_sequence,
1134        checkpoint_digest: anchor
1135            .checkpoint_digest
1136            .as_ref()
1137            .map(|digest| encode_hex(digest)),
1138        snapshot_digest: snapshot_digest.clone(),
1139        witness: WitnessV1 {
1140            path: format!(
1141                "/v1/witnesses/{}/{}",
1142                anchor.checkpoint_sequence, snapshot_digest
1143            ),
1144            file_bytes: artifact.snapshot.file_bytes,
1145        },
1146    })
1147}
1148
1149fn lexical_retrieval_proof_transport(
1150    artifact: &LexicalRetrievalProofArtifact,
1151    state: &ServerState,
1152    request_id: &str,
1153) -> Result<RetrievalProofV1, ApiError> {
1154    retrieval_proof_transport_parts(
1155        artifact.proof.to_bytes(),
1156        artifact.proof.proof_digest(),
1157        artifact.proof.anchor_digest(),
1158        artifact.proof.anchor(),
1159        artifact.snapshot.file_bytes,
1160        state,
1161        request_id,
1162    )
1163}
1164
1165fn hybrid_retrieval_proof_transport(
1166    artifact: &HybridRetrievalProofArtifact,
1167    state: &ServerState,
1168    request_id: &str,
1169) -> Result<RetrievalProofV1, ApiError> {
1170    retrieval_proof_transport_parts(
1171        artifact.proof.to_bytes(),
1172        artifact.proof.proof_digest(),
1173        artifact.proof.anchor_digest(),
1174        artifact.proof.anchor(),
1175        artifact.snapshot.file_bytes,
1176        state,
1177        request_id,
1178    )
1179}
1180
1181#[allow(clippy::too_many_arguments)]
1182fn retrieval_proof_transport_parts(
1183    encoded: Result<Vec<u8>, hyphae_engine::RetrievalProofError>,
1184    proof_digest: [u8; 32],
1185    anchor_digest: [u8; 32],
1186    anchor: &hyphae_engine::RetrievalProofAnchor,
1187    witness_bytes: u64,
1188    state: &ServerState,
1189    request_id: &str,
1190) -> Result<RetrievalProofV1, ApiError> {
1191    let encoded = encoded
1192        .map_err(|source| ApiError::from_engine(EngineError::RetrievalProof(source), request_id))?;
1193    if encoded.len() > state.limits.proof_bytes || witness_bytes > state.limits.witness_bytes {
1194        return Err(ApiError::result_too_large(request_id));
1195    }
1196    let snapshot_digest = encode_hex(&anchor.snapshot_digest);
1197    Ok(RetrievalProofV1 {
1198        encoding: "base64".to_owned(),
1199        data: BASE64.encode(encoded),
1200        proof_digest: encode_hex(&proof_digest),
1201        anchor_digest: encode_hex(&anchor_digest),
1202        checkpoint_sequence: anchor.checkpoint_sequence,
1203        checkpoint_digest: anchor
1204            .checkpoint_digest
1205            .as_ref()
1206            .map(|digest| encode_hex(digest)),
1207        snapshot_digest: snapshot_digest.clone(),
1208        witness: WitnessV1 {
1209            path: format!(
1210                "/v1/witnesses/{}/{}",
1211                anchor.checkpoint_sequence, snapshot_digest
1212            ),
1213            file_bytes: witness_bytes,
1214        },
1215    })
1216}
1217
1218fn exact_outcome_transport(outcome: &ExactRetrievalOutcome) -> ExactRetrievalOutcomeV1 {
1219    match outcome {
1220        ExactRetrievalOutcome::Matches {
1221            matches,
1222            scanned_candidates,
1223        } => ExactRetrievalOutcomeV1::Matches {
1224            matches: matches
1225                .iter()
1226                .map(|matched| ExactRetrievalMatchV1 {
1227                    key_hex: encode_hex(&matched.key),
1228                    score_nanos: matched.score_nanos,
1229                })
1230                .collect(),
1231            scanned_candidates: *scanned_candidates,
1232        },
1233        ExactRetrievalOutcome::Abstained(abstention) => ExactRetrievalOutcomeV1::Abstained {
1234            abstention: ExactAbstentionV1 {
1235                reason: match abstention.reason {
1236                    ExactAbstentionReason::NoCandidates => ExactAbstentionReasonV1::NoCandidates,
1237                    ExactAbstentionReason::BelowThreshold => {
1238                        ExactAbstentionReasonV1::BelowThreshold
1239                    }
1240                    ExactAbstentionReason::Ambiguous => ExactAbstentionReasonV1::Ambiguous,
1241                },
1242                best_score_nanos: abstention.best_score_nanos,
1243                runner_up_score_nanos: abstention.runner_up_score_nanos,
1244                scanned_candidates: abstention.scanned_candidates,
1245            },
1246        },
1247    }
1248}
1249
1250fn lexical_outcome_transport(outcome: &LexicalOutcome) -> LexicalRetrievalOutcomeV1 {
1251    match outcome {
1252        LexicalOutcome::Matches {
1253            matches,
1254            scanned_documents,
1255            matched_documents,
1256            query_tokens,
1257        } => LexicalRetrievalOutcomeV1::Matches {
1258            matches: matches
1259                .iter()
1260                .map(|matched| LexicalRetrievalMatchV1 {
1261                    key_hex: encode_hex(&matched.key),
1262                    score_nanos: matched.score_nanos,
1263                    terms: matched
1264                        .terms
1265                        .iter()
1266                        .map(|term| LexicalTermContributionV1 {
1267                            token: term.token.clone(),
1268                            document_frequency: term.document_frequency,
1269                            score_nanos: term.score_nanos,
1270                            fields: term
1271                                .fields
1272                                .iter()
1273                                .map(|field| LexicalFieldContributionV1 {
1274                                    path: field.path.segments().to_vec(),
1275                                    term_frequency: field.term_frequency,
1276                                    field_length: field.field_length,
1277                                })
1278                                .collect(),
1279                        })
1280                        .collect(),
1281                })
1282                .collect(),
1283            scanned_documents: *scanned_documents,
1284            matched_documents: *matched_documents,
1285            query_tokens: query_tokens.clone(),
1286        },
1287        LexicalOutcome::Abstained(abstention) => LexicalRetrievalOutcomeV1::Abstained {
1288            abstention: LexicalAbstentionV1 {
1289                reason: match abstention.reason {
1290                    LexicalAbstentionReason::NoCandidates => {
1291                        LexicalAbstentionReasonV1::NoCandidates
1292                    }
1293                },
1294                scanned_documents: abstention.scanned_documents,
1295                query_tokens: abstention.query_tokens.clone(),
1296            },
1297        },
1298    }
1299}
1300
1301fn hybrid_outcome_transport(outcome: &HybridOutcome) -> HybridRetrievalOutcomeV1 {
1302    match outcome {
1303        HybridOutcome::Matches {
1304            matches,
1305            lexical_absence,
1306            vector_absence,
1307        } => HybridRetrievalOutcomeV1::Matches {
1308            matches: matches
1309                .iter()
1310                .map(|matched| HybridRetrievalMatchV1 {
1311                    key_hex: encode_hex(&matched.key),
1312                    explanation: HybridExplanationV1 {
1313                        lexical_rank: matched.explanation.lexical_rank,
1314                        lexical_score_nanos: matched.explanation.lexical_score_nanos,
1315                        vector_rank: matched.explanation.vector_rank,
1316                        vector_score_nanos: matched.explanation.vector_score_nanos,
1317                        lexical_contribution: matched.explanation.lexical_contribution,
1318                        vector_contribution: matched.explanation.vector_contribution,
1319                        fusion_score: matched.explanation.fusion_score,
1320                        final_rank: matched.explanation.final_rank,
1321                    },
1322                })
1323                .collect(),
1324            lexical_absence: lexical_absence.map(hybrid_absence_transport),
1325            vector_absence: vector_absence.map(hybrid_absence_transport),
1326        },
1327        HybridOutcome::Abstained(abstention) => HybridRetrievalOutcomeV1::Abstained {
1328            abstention: HybridAbstentionV1 {
1329                lexical: hybrid_absence_transport(abstention.lexical),
1330                vector: hybrid_absence_transport(abstention.vector),
1331            },
1332        },
1333    }
1334}
1335
1336fn hybrid_absence_transport(absence: HybridBranchAbsence) -> HybridBranchAbsenceV1 {
1337    match absence {
1338        HybridBranchAbsence::LexicalNoCandidates => HybridBranchAbsenceV1::LexicalNoCandidates,
1339        HybridBranchAbsence::VectorNoCandidates => HybridBranchAbsenceV1::VectorNoCandidates,
1340        HybridBranchAbsence::VectorBelowThreshold => HybridBranchAbsenceV1::VectorBelowThreshold,
1341        HybridBranchAbsence::VectorAmbiguous => HybridBranchAbsenceV1::VectorAmbiguous,
1342    }
1343}
1344
1345fn receipt(outcome: AppendOutcome) -> CommitReceiptV1 {
1346    let (status, receipt) = match outcome {
1347        AppendOutcome::Committed(receipt) => ("committed", receipt),
1348        AppendOutcome::Existing(receipt) => ("existing", receipt),
1349    };
1350    CommitReceiptV1 {
1351        status: status.to_owned(),
1352        transaction_id: receipt.transaction_id.to_string(),
1353        commit_sequence: receipt.commit_sequence,
1354        commit_digest: encode_hex(&receipt.commit_digest),
1355        transaction_digest: encode_hex(&receipt.transaction_digest),
1356    }
1357}
1358
1359fn bounded_json<T: Serialize>(
1360    value: &T,
1361    state: &ServerState,
1362    request_id: &str,
1363) -> Result<Response, ApiError> {
1364    let encoded = serde_json::to_vec(value).map_err(|_| ApiError::internal(request_id))?;
1365    if encoded.len() > state.limits.response_bytes {
1366        return Err(ApiError::result_too_large(request_id));
1367    }
1368    Response::builder()
1369        .status(StatusCode::OK)
1370        .header(header::CONTENT_TYPE, "application/json")
1371        .header(header::CONTENT_LENGTH, encoded.len())
1372        .body(Body::from(encoded))
1373        .map_err(|_| ApiError::internal(request_id))
1374}
1375
1376fn parse_witness_path(path: &str) -> Option<(u64, [u8; 32])> {
1377    let suffix = path.strip_prefix("/v1/witnesses/")?;
1378    let mut components = suffix.split('/');
1379    let sequence = components.next()?.parse().ok()?;
1380    let digest = components.next()?;
1381    if components.next().is_some()
1382        || digest.len() != 64
1383        || !digest
1384            .bytes()
1385            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1386    {
1387        return None;
1388    }
1389    let decoded = decode_key_hex(digest).ok()?;
1390    decoded.try_into().ok().map(|digest| (sequence, digest))
1391}
1392
1393fn request_id(request: &Request) -> String {
1394    request
1395        .extensions()
1396        .get::<RequestId>()
1397        .map_or_else(|| Uuid::now_v7().to_string(), |value| value.0.clone())
1398}
1399
1400fn busy(request_id: &str) -> ApiError {
1401    ApiError::new(
1402        StatusCode::TOO_MANY_REQUESTS,
1403        "busy",
1404        "concurrent operation admission limit reached",
1405        request_id,
1406    )
1407}
1408
1409fn not_found(request_id: &str) -> ApiError {
1410    ApiError::new(
1411        StatusCode::NOT_FOUND,
1412        "not_found",
1413        "requested version 1 resource does not exist",
1414        request_id,
1415    )
1416}
1417
1418async fn route_not_found(Extension(request_id): Extension<RequestId>) -> ApiError {
1419    not_found(&request_id.0)
1420}
1421
1422async fn method_not_allowed(Extension(request_id): Extension<RequestId>) -> ApiError {
1423    ApiError::new(
1424        StatusCode::METHOD_NOT_ALLOWED,
1425        "method_not_allowed",
1426        "HTTP method is not defined for this version 1 route",
1427        request_id.0,
1428    )
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433    use std::{
1434        error::Error,
1435        fs,
1436        io::Write as _,
1437        net::Ipv4Addr,
1438        path::PathBuf,
1439        sync::{Arc, atomic::Ordering},
1440        time::Duration,
1441    };
1442
1443    use axum::{
1444        body::Body,
1445        http::{Request, header},
1446    };
1447    use serde_json::Value;
1448    use tokio::{
1449        io::{AsyncReadExt as _, AsyncWriteExt as _},
1450        net::TcpStream,
1451        sync::oneshot,
1452    };
1453    use tokio_util::io::ReaderStream;
1454    use tower::ServiceExt;
1455
1456    use hyphae_engine::EngineError;
1457    use hyphae_storage::{AppendOutcome, CommitReceipt, MaterializedIndexError, StorageError};
1458
1459    use super::{HyphaeServer, ServerConfig, StatusCode, body, capture_write_outcome};
1460    use crate::{BearerToken, ServerConfigError};
1461
1462    struct TestDirectory {
1463        path: PathBuf,
1464    }
1465
1466    impl TestDirectory {
1467        fn create(name: &str) -> Result<Self, Box<dyn Error>> {
1468            let path = std::env::temp_dir().join(format!(
1469                "hyphae-server-{name}-{}-{}",
1470                std::process::id(),
1471                uuid::Uuid::now_v7()
1472            ));
1473            fs::create_dir_all(&path)?;
1474            Ok(Self { path })
1475        }
1476    }
1477
1478    impl Drop for TestDirectory {
1479        fn drop(&mut self) {
1480            let _ignored = fs::remove_dir_all(&self.path);
1481        }
1482    }
1483
1484    #[test]
1485    fn remote_bind_is_rejected_before_socket_bind() -> Result<(), Box<dyn Error>> {
1486        let temporary = TestDirectory::create("remote-rejected")?;
1487        let mut config = ServerConfig::new(&temporary.path);
1488        config.bind = (Ipv4Addr::UNSPECIFIED, 8_787).into();
1489        assert!(matches!(
1490            HyphaeServer::open(config),
1491            Err(crate::ServerError::Configuration(
1492                ServerConfigError::RemoteBindRequiresAuthentication { .. }
1493            ))
1494        ));
1495        Ok(())
1496    }
1497
1498    #[test]
1499    fn bearer_tokens_require_visible_header_safe_entropy() {
1500        assert!(BearerToken::new("short").is_err());
1501        assert!(BearerToken::new("0123456789abcdef0123456789abcde\n").is_err());
1502        assert!(BearerToken::new("0123456789abcdef0123456789abcdef").is_ok());
1503    }
1504
1505    #[test]
1506    fn durable_unmaterialized_commit_keeps_its_public_receipt() -> Result<(), Box<dyn Error>> {
1507        let receipt = CommitReceipt {
1508            transaction_id: uuid::Uuid::now_v7(),
1509            commit_sequence: 9,
1510            commit_digest: [7; 32],
1511            transaction_digest: [8; 32],
1512        };
1513        let outcome = capture_write_outcome(Err(EngineError::Storage(
1514            StorageError::CommittedButNotIndexed {
1515                receipt,
1516                source: Box::new(MaterializedIndexError::MalformedCheckpoint),
1517            },
1518        )))?;
1519        assert!(outcome.requires_recovery);
1520        assert!(matches!(
1521            outcome.append,
1522            AppendOutcome::Committed(actual) if actual == receipt
1523        ));
1524        Ok(())
1525    }
1526
1527    #[tokio::test]
1528    async fn oversized_witness_is_rejected_by_metadata_before_hashing() -> Result<(), Box<dyn Error>>
1529    {
1530        let temporary = TestDirectory::create("oversized-witness")?;
1531        let mut config = ServerConfig::new(&temporary.path);
1532        config.limits.witness_bytes = 8;
1533        let app = HyphaeServer::open(config)?.test_router();
1534        let witness_path = temporary
1535            .path
1536            .join("snapshots")
1537            .join("snapshot-00000000000000000001.hysnap");
1538        fs::write(&witness_path, [0_u8; 9])?;
1539
1540        let response = app
1541            .oneshot(
1542                Request::builder()
1543                    .uri(format!("/v1/witnesses/1/{}", "00".repeat(32)))
1544                    .body(Body::empty())?,
1545            )
1546            .await?;
1547        assert_error(response, StatusCode::PAYLOAD_TOO_LARGE, "result_too_large").await?;
1548        Ok(())
1549    }
1550
1551    #[tokio::test]
1552    async fn snapshot_policy_failure_does_not_drop_server_readiness() -> Result<(), Box<dyn Error>>
1553    {
1554        let temporary = TestDirectory::create("snapshot-policy-readiness")?;
1555        let mut config = ServerConfig::new(&temporary.path);
1556        config.limits.witness_bytes = 1;
1557        let server = HyphaeServer::open(config)?;
1558        let app = server.test_router();
1559
1560        let put = app
1561            .clone()
1562            .oneshot(json_request(
1563                "/v1/kv/put",
1564                r#"{"records":[{"key_hex":"61","value":1}]}"#,
1565                None,
1566            )?)
1567            .await?;
1568        assert_eq!(put.status(), StatusCode::OK);
1569
1570        let rejected = app
1571            .clone()
1572            .oneshot(json_request("/v1/kv/get", r#"{"key_hex":"61"}"#, None)?)
1573            .await?;
1574        assert_error(rejected, StatusCode::PAYLOAD_TOO_LARGE, "result_too_large").await?;
1575        assert!(server.state.ready.load(Ordering::Acquire));
1576
1577        let ready = app
1578            .oneshot(
1579                Request::builder()
1580                    .uri("/v1/health/ready")
1581                    .body(Body::empty())?,
1582            )
1583            .await?;
1584        assert_eq!(ready.status(), StatusCode::OK);
1585        Ok(())
1586    }
1587
1588    #[tokio::test]
1589    async fn witness_stream_stops_at_verified_length_after_late_append()
1590    -> Result<(), Box<dyn Error>> {
1591        let temporary = TestDirectory::create("witness-stream-length")?;
1592        let app = HyphaeServer::open(ServerConfig::new(&temporary.path))?.test_router();
1593        let put = app
1594            .clone()
1595            .oneshot(json_request(
1596                "/v1/kv/put",
1597                r#"{"transaction_id":"018f0000-0000-7000-8000-000000000011","records":[{"key_hex":"61","value":1}]}"#,
1598                None,
1599            )?)
1600            .await?;
1601        assert_eq!(put.status(), StatusCode::OK);
1602
1603        let get = app
1604            .clone()
1605            .oneshot(json_request("/v1/kv/get", r#"{"key_hex":"61"}"#, None)?)
1606            .await?;
1607        assert_eq!(get.status(), StatusCode::OK);
1608        let get: Value = serde_json::from_slice(&response_bytes(get).await?)?;
1609        let witness_path = get["proof"]["witness"]["path"]
1610            .as_str()
1611            .ok_or("missing witness path")?
1612            .to_owned();
1613        let witness = app
1614            .oneshot(Request::builder().uri(witness_path).body(Body::empty())?)
1615            .await?;
1616        assert_eq!(witness.status(), StatusCode::OK);
1617        let verified_length = witness
1618            .headers()
1619            .get(header::CONTENT_LENGTH)
1620            .ok_or("missing witness content length")?
1621            .to_str()?
1622            .parse::<usize>()?;
1623
1624        let mut snapshots = fs::read_dir(temporary.path.join("snapshots"))?;
1625        let snapshot_path = snapshots.next().ok_or("missing local snapshot")??.path();
1626        assert!(snapshots.next().is_none());
1627        let suffix = b"late-unverified-growth";
1628        let mut snapshot = fs::OpenOptions::new().append(true).open(snapshot_path)?;
1629        snapshot.write_all(suffix)?;
1630        snapshot.sync_all()?;
1631        drop(snapshot);
1632
1633        let streamed = response_bytes(witness).await?;
1634        assert_eq!(streamed.len(), verified_length);
1635        assert!(!streamed.ends_with(suffix));
1636        Ok(())
1637    }
1638
1639    #[tokio::test]
1640    #[allow(clippy::too_many_lines)]
1641    async fn authenticated_put_get_and_witness_are_contract_shaped() -> Result<(), Box<dyn Error>> {
1642        let temporary = TestDirectory::create("authenticated-flow")?;
1643        let secret = "correct-hyphae-token-material-0001";
1644        let mut config = ServerConfig::new(&temporary.path);
1645        config.bearer_token = Some(BearerToken::new(secret)?);
1646        config.limits.concurrent_operations = 1;
1647        let app = HyphaeServer::open(config)?.test_router();
1648
1649        let unauthorized = app
1650            .clone()
1651            .oneshot(json_request("/v1/kv/put", r#"{"records":[]}"#, None)?)
1652            .await?;
1653        assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
1654        assert!(unauthorized.headers().contains_key("x-request-id"));
1655
1656        let wrong = app
1657            .clone()
1658            .oneshot(json_request(
1659                "/v1/kv/put",
1660                r#"{"records":[]}"#,
1661                Some("incorrect-hyphae-token-material-001"),
1662            )?)
1663            .await?;
1664        assert_error(wrong, StatusCode::UNAUTHORIZED, "unauthorized").await?;
1665
1666        let duplicate_header = app
1667            .clone()
1668            .oneshot(
1669                Request::builder()
1670                    .method("POST")
1671                    .uri("/v1/query")
1672                    .header("content-type", "application/json")
1673                    .header("authorization", format!("Bearer {secret}"))
1674                    .header("authorization", format!("Bearer {secret}"))
1675                    .body(Body::from(r#"{"limit":1}"#))?,
1676            )
1677            .await?;
1678        assert_error(duplicate_header, StatusCode::UNAUTHORIZED, "unauthorized").await?;
1679
1680        let put = app
1681            .clone()
1682            .oneshot(json_request(
1683                "/v1/kv/put",
1684                r#"{"transaction_id":"018f0000-0000-7000-8000-000000000001","records":[{"key_hex":"61","value":{"score":7}}]}"#,
1685                Some(secret),
1686            )?)
1687            .await?;
1688        assert_eq!(put.status(), StatusCode::OK);
1689        let put: Value = serde_json::from_slice(&response_bytes(put).await?)?;
1690        assert_eq!(put["status"], "committed");
1691
1692        let retry = app
1693            .clone()
1694            .oneshot(json_request(
1695                "/v1/kv/put",
1696                r#"{"transaction_id":"018f0000-0000-7000-8000-000000000001","records":[{"key_hex":"61","value":{"score":7}}]}"#,
1697                Some(secret),
1698            )?)
1699            .await?;
1700        assert_eq!(retry.status(), StatusCode::OK);
1701        let retry: Value = serde_json::from_slice(&response_bytes(retry).await?)?;
1702        assert_eq!(retry["status"], "existing");
1703
1704        let conflict = app
1705            .clone()
1706            .oneshot(json_request(
1707                "/v1/kv/put",
1708                r#"{"transaction_id":"018f0000-0000-7000-8000-000000000001","records":[{"key_hex":"61","value":{"score":8}}]}"#,
1709                Some(secret),
1710            )?)
1711            .await?;
1712        assert_error(conflict, StatusCode::CONFLICT, "idempotency_conflict").await?;
1713
1714        let get = app
1715            .clone()
1716            .oneshot(json_request(
1717                "/v1/kv/get",
1718                r#"{"key_hex":"61"}"#,
1719                Some(secret),
1720            )?)
1721            .await?;
1722        assert_eq!(get.status(), StatusCode::OK);
1723        let get: Value = serde_json::from_slice(&response_bytes(get).await?)?;
1724        assert_eq!(get["found"], true);
1725        assert_eq!(get["record"]["value"]["score"], 7);
1726        assert_eq!(get["proof"]["encoding"], "base64");
1727        let witness_path = get["proof"]["witness"]["path"]
1728            .as_str()
1729            .ok_or("missing witness path")?;
1730
1731        let witness = app
1732            .clone()
1733            .oneshot(
1734                Request::builder()
1735                    .uri(witness_path)
1736                    .header("authorization", format!("Bearer {secret}"))
1737                    .body(Body::empty())?,
1738            )
1739            .await?;
1740        assert_eq!(witness.status(), StatusCode::OK);
1741        assert!(witness.headers().contains_key("digest"));
1742        let blocked_while_stream_is_live = app
1743            .clone()
1744            .oneshot(json_request("/v1/query", r#"{"limit":1}"#, Some(secret))?)
1745            .await?;
1746        assert_error(
1747            blocked_while_stream_is_live,
1748            StatusCode::TOO_MANY_REQUESTS,
1749            "busy",
1750        )
1751        .await?;
1752        assert!(response_bytes(witness).await?.starts_with(b"HYSNAP01"));
1753
1754        let query = app
1755            .oneshot(json_request("/v1/query", r#"{"limit":10}"#, Some(secret))?)
1756            .await?;
1757        assert_eq!(query.status(), StatusCode::OK);
1758        let query: Value = serde_json::from_slice(&response_bytes(query).await?)?;
1759        assert_eq!(query["rows"].as_array().map(Vec::len), Some(1));
1760        assert_eq!(query["proof"]["encoding"], "base64");
1761        Ok(())
1762    }
1763
1764    #[tokio::test]
1765    async fn public_routes_and_limit_failures_never_emit_framework_text()
1766    -> Result<(), Box<dyn Error>> {
1767        let temporary = TestDirectory::create("limits")?;
1768        let mut config = ServerConfig::new(&temporary.path);
1769        config.limits.request_body_bytes = 1_024;
1770        config.limits.batch_items = 1;
1771        let app = HyphaeServer::open(config)?.test_router();
1772
1773        let capabilities = app
1774            .clone()
1775            .oneshot(
1776                Request::builder()
1777                    .uri("/v1/capabilities")
1778                    .body(Body::empty())?,
1779            )
1780            .await?;
1781        assert_eq!(capabilities.status(), StatusCode::OK);
1782        let capabilities: Value = serde_json::from_slice(&response_bytes(capabilities).await?)?;
1783        assert_eq!(capabilities["api_version"], "v1");
1784        assert_eq!(capabilities["limits"]["batch_items"], 1);
1785
1786        let too_many = app
1787            .clone()
1788            .oneshot(json_request(
1789                "/v1/kv/delete",
1790                r#"{"keys_hex":["61","62"]}"#,
1791                None,
1792            )?)
1793            .await?;
1794        assert_error(too_many, StatusCode::UNPROCESSABLE_ENTITY, "limit_exceeded").await?;
1795
1796        let unsupported = app
1797            .clone()
1798            .oneshot(
1799                Request::builder()
1800                    .method("POST")
1801                    .uri("/v1/query")
1802                    .header("content-type", "text/plain")
1803                    .body(Body::from("{}"))?,
1804            )
1805            .await?;
1806        assert_error(
1807            unsupported,
1808            StatusCode::UNSUPPORTED_MEDIA_TYPE,
1809            "unsupported_media_type",
1810        )
1811        .await?;
1812
1813        let oversized = app
1814            .clone()
1815            .oneshot(json_request(
1816                "/v1/query",
1817                &format!(r#"{{"limit":1,"ignored":"{}"}}"#, "x".repeat(2_000)),
1818                None,
1819            )?)
1820            .await?;
1821        assert_error(
1822            oversized,
1823            StatusCode::PAYLOAD_TOO_LARGE,
1824            "payload_too_large",
1825        )
1826        .await?;
1827
1828        let missing = app
1829            .clone()
1830            .oneshot(Request::builder().uri("/v1/unknown").body(Body::empty())?)
1831            .await?;
1832        assert_error(missing, StatusCode::NOT_FOUND, "not_found").await?;
1833
1834        let wrong_method = app
1835            .oneshot(
1836                Request::builder()
1837                    .method("DELETE")
1838                    .uri("/v1/health/live")
1839                    .body(Body::empty())?,
1840            )
1841            .await?;
1842        assert_error(
1843            wrong_method,
1844            StatusCode::METHOD_NOT_ALLOWED,
1845            "method_not_allowed",
1846        )
1847        .await?;
1848        Ok(())
1849    }
1850
1851    #[tokio::test]
1852    async fn vector_lexical_and_hybrid_routes_return_proof_bearing_results()
1853    -> Result<(), Box<dyn Error>> {
1854        let temporary = TestDirectory::create("retrieval-flow")?;
1855        let app = HyphaeServer::open(ServerConfig::new(&temporary.path))?.test_router();
1856
1857        for (path, payload) in [
1858            (
1859                "/v1/kv/put",
1860                r#"{"records":[{"key_hex":"616c706861","value":{"title":"Durable memory","body":"offline agent memory"}},{"key_hex":"62657461","value":{"title":"Fast search","body":"exact vector retrieval"}}]}"#,
1861            ),
1862            (
1863                "/v1/lexical-indexes/define",
1864                r#"{"lexical_index":{"name":"content","fields":[{"path":["title"],"weight_micros":2000000},{"path":["body"],"weight_micros":1000000}]}}"#,
1865            ),
1866            (
1867                "/v1/vector-spaces/define",
1868                r#"{"vector_space":{"name":"semantic","dimension":2,"metric":"cosine_q15_nanos"}}"#,
1869            ),
1870            (
1871                "/v1/vectors/put",
1872                r#"{"vector_space":"semantic","vectors":[{"key_hex":"616c706861","values":[32767,0]},{"key_hex":"62657461","values":[0,32767]}]}"#,
1873            ),
1874        ] {
1875            let response = app
1876                .clone()
1877                .oneshot(json_request(path, payload, None)?)
1878                .await?;
1879            assert_eq!(response.status(), StatusCode::OK, "{path}");
1880        }
1881
1882        let exact = app
1883            .clone()
1884            .oneshot(json_request(
1885                "/v1/retrieve/exact",
1886                r#"{"vector_space":"semantic","query":[32767,0],"limit":2,"minimum_score_nanos":-1000000000,"minimum_margin_nanos":0}"#,
1887                None,
1888            )?)
1889            .await?;
1890        assert_eq!(exact.status(), StatusCode::OK);
1891        let exact: Value = serde_json::from_slice(&response_bytes(exact).await?)?;
1892        assert_eq!(exact["outcome"]["matches"][0]["key_hex"], "616c706861");
1893        assert_eq!(exact["proof"]["encoding"], "base64");
1894
1895        let lexical = app
1896            .clone()
1897            .oneshot(json_request(
1898                "/v1/retrieve/lexical",
1899                r#"{"lexical_index":"content","query":"durable memory","limit":2}"#,
1900                None,
1901            )?)
1902            .await?;
1903        assert_eq!(lexical.status(), StatusCode::OK);
1904        let lexical: Value = serde_json::from_slice(&response_bytes(lexical).await?)?;
1905        assert_eq!(lexical["outcome"]["matches"][0]["key_hex"], "616c706861");
1906        assert_eq!(lexical["proof"]["encoding"], "base64");
1907
1908        let hybrid = app
1909            .clone()
1910            .oneshot(json_request(
1911                "/v1/retrieve/hybrid",
1912                r#"{"lexical":{"lexical_index":"content","query":"durable memory","limit":2},"vector":{"vector_space":"semantic","query":[32767,0],"limit":2,"minimum_score_nanos":-1000000000,"minimum_margin_nanos":0},"lexical_weight":1,"vector_weight":1,"limit":2}"#,
1913                None,
1914            )?)
1915            .await?;
1916        assert_eq!(hybrid.status(), StatusCode::OK);
1917        let hybrid: Value = serde_json::from_slice(&response_bytes(hybrid).await?)?;
1918        assert_eq!(hybrid["outcome"]["matches"][0]["key_hex"], "616c706861");
1919        assert_eq!(
1920            hybrid["outcome"]["matches"][0]["explanation"]["final_rank"],
1921            1
1922        );
1923        assert_eq!(hybrid["proof"]["encoding"], "base64");
1924
1925        let wrong_dimension = app
1926            .clone()
1927            .oneshot(json_request(
1928                "/v1/retrieve/exact",
1929                r#"{"vector_space":"semantic","query":[32767],"limit":2,"minimum_score_nanos":-1000000000,"minimum_margin_nanos":0}"#,
1930                None,
1931            )?)
1932            .await?;
1933        assert_error(wrong_dimension, StatusCode::BAD_REQUEST, "invalid_request").await?;
1934
1935        let empty_query = app
1936            .oneshot(json_request(
1937                "/v1/retrieve/lexical",
1938                r#"{"lexical_index":"content","query":"---","limit":2}"#,
1939                None,
1940            )?)
1941            .await?;
1942        assert_error(empty_query, StatusCode::BAD_REQUEST, "invalid_request").await?;
1943        Ok(())
1944    }
1945
1946    #[tokio::test]
1947    async fn shape_proof_and_admission_limits_fail_without_partial_results()
1948    -> Result<(), Box<dyn Error>> {
1949        let temporary = TestDirectory::create("bounded-work")?;
1950        let mut config = ServerConfig::new(&temporary.path);
1951        config.limits.json_depth = 3;
1952        config.limits.concurrent_operations = 1;
1953        config.limits.proof_bytes = 128;
1954        let server = HyphaeServer::open(config)?;
1955        let app = server.test_router();
1956
1957        let too_deep = app
1958            .clone()
1959            .oneshot(json_request(
1960                "/v1/query",
1961                r#"{"filter":{"op":"not","filter":{"op":"not","filter":{"op":"not","filter":{"op":"match_all"}}}},"limit":1}"#,
1962                None,
1963            )?)
1964            .await?;
1965        assert_error(too_deep, StatusCode::UNPROCESSABLE_ENTITY, "limit_exceeded").await?;
1966
1967        let put = app
1968            .clone()
1969            .oneshot(json_request(
1970                "/v1/kv/put",
1971                r#"{"records":[{"key_hex":"61","value":1}]}"#,
1972                None,
1973            )?)
1974            .await?;
1975        assert_eq!(put.status(), StatusCode::OK);
1976
1977        let proof_too_large = app
1978            .clone()
1979            .oneshot(json_request("/v1/kv/get", r#"{"key_hex":"61"}"#, None)?)
1980            .await?;
1981        assert_error(
1982            proof_too_large,
1983            StatusCode::PAYLOAD_TOO_LARGE,
1984            "result_too_large",
1985        )
1986        .await?;
1987
1988        let permit = Arc::clone(&server.state.admission).try_acquire_owned()?;
1989        let busy = app
1990            .clone()
1991            .oneshot(json_request("/v1/query", r#"{"limit":1}"#, None)?)
1992            .await?;
1993        drop(permit);
1994        assert_error(busy, StatusCode::TOO_MANY_REQUESTS, "busy").await?;
1995
1996        server
1997            .state
1998            .ready
1999            .store(false, std::sync::atomic::Ordering::Release);
2000        let unavailable = app
2001            .oneshot(
2002                Request::builder()
2003                    .uri("/v1/health/ready")
2004                    .body(Body::empty())?,
2005            )
2006            .await?;
2007        assert_error(unavailable, StatusCode::SERVICE_UNAVAILABLE, "unavailable").await?;
2008        Ok(())
2009    }
2010
2011    #[tokio::test]
2012    async fn stalled_json_body_times_out_before_any_operation_starts() -> Result<(), Box<dyn Error>>
2013    {
2014        let temporary = TestDirectory::create("body-timeout")?;
2015        let mut config = ServerConfig::new(&temporary.path);
2016        config.limits.request_body_timeout = Duration::from_millis(5);
2017        let app = HyphaeServer::open(config)?.test_router();
2018        let (_writer, reader) = tokio::io::duplex(1);
2019        let response = app
2020            .oneshot(
2021                Request::builder()
2022                    .method("POST")
2023                    .uri("/v1/query")
2024                    .header("content-type", "application/json")
2025                    .body(Body::from_stream(ReaderStream::new(reader)))?,
2026            )
2027            .await?;
2028        assert_error(response, StatusCode::REQUEST_TIMEOUT, "timeout").await?;
2029        Ok(())
2030    }
2031
2032    #[tokio::test]
2033    async fn bound_server_stops_on_graceful_shutdown() -> Result<(), Box<dyn Error>> {
2034        let temporary = TestDirectory::create("graceful")?;
2035        let mut config = ServerConfig::new(&temporary.path);
2036        config.bind.set_port(0);
2037        let bound = HyphaeServer::open(config)?.bind().await?;
2038        let local_addr = bound.local_addr();
2039        assert_ne!(local_addr.port(), 0);
2040        let (send, receive) = oneshot::channel::<()>();
2041        let serving = tokio::spawn(bound.run_with_shutdown(async move {
2042            let _ignored = receive.await;
2043        }));
2044        let mut connection = TcpStream::connect(local_addr).await?;
2045        connection
2046            .write_all(
2047                b"GET /v1/health/live HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
2048            )
2049            .await?;
2050        let mut response = Vec::new();
2051        connection.read_to_end(&mut response).await?;
2052        assert!(response.starts_with(b"HTTP/1.1 200 OK\r\n"));
2053        assert!(response.ends_with(br#"{"status":"live"}"#));
2054        let _ignored = send.send(());
2055        serving.await??;
2056        Ok(())
2057    }
2058
2059    fn json_request(
2060        uri: &str,
2061        body: &str,
2062        bearer: Option<&str>,
2063    ) -> Result<Request<Body>, axum::http::Error> {
2064        let mut request = Request::builder()
2065            .method("POST")
2066            .uri(uri)
2067            .header("content-type", "application/json");
2068        if let Some(bearer) = bearer {
2069            request = request.header("authorization", format!("Bearer {bearer}"));
2070        }
2071        request.body(Body::from(body.to_owned()))
2072    }
2073
2074    async fn response_bytes(
2075        response: axum::response::Response,
2076    ) -> Result<axum::body::Bytes, Box<dyn Error>> {
2077        Ok(body::to_bytes(response.into_body(), 64 * 1024 * 1024).await?)
2078    }
2079
2080    async fn assert_error(
2081        response: axum::response::Response,
2082        status: StatusCode,
2083        code: &str,
2084    ) -> Result<(), Box<dyn Error>> {
2085        assert_eq!(response.status(), status);
2086        let header_request_id = response
2087            .headers()
2088            .get("x-request-id")
2089            .and_then(|value| value.to_str().ok())
2090            .ok_or("missing request ID header")?
2091            .to_owned();
2092        let value: Value = serde_json::from_slice(&response_bytes(response).await?)?;
2093        assert_eq!(value["code"], code);
2094        assert_eq!(value["request_id"], header_request_id);
2095        Ok(())
2096    }
2097}