Skip to main content

alopex_server/http/
mod.rs

1pub mod admin;
2pub mod admin_api;
3pub mod admin_resources;
4pub mod columnar;
5pub mod hnsw;
6pub mod kv;
7pub mod session;
8pub mod sql;
9pub mod vector;
10
11use std::sync::atomic::Ordering;
12use std::sync::Arc;
13
14use axum::http::{HeaderValue, StatusCode};
15use axum::middleware;
16use axum::response::{IntoResponse, Response};
17use axum::{Json, Router};
18use serde::Serialize;
19use tower::ServiceBuilder;
20use tower_http::limit::RequestBodyLimitLayer;
21use tower_http::trace::TraceLayer;
22use tracing::Span;
23use uuid::Uuid;
24
25use crate::auth::AuthError;
26use crate::error::ServerError;
27use crate::server::ServerState;
28
29#[derive(Clone, Debug)]
30pub struct RequestContext {
31    pub correlation_id: String,
32    pub actor: Option<String>,
33}
34
35#[derive(Serialize)]
36struct ErrorBody {
37    code: String,
38    message: String,
39    correlation_id: String,
40}
41
42#[derive(Serialize)]
43struct ErrorResponse {
44    error: ErrorBody,
45}
46
47struct QueueWaitGuard<'a> {
48    counter: &'a std::sync::atomic::AtomicUsize,
49}
50
51impl<'a> QueueWaitGuard<'a> {
52    fn new(counter: &'a std::sync::atomic::AtomicUsize) -> Self {
53        Self { counter }
54    }
55}
56
57impl Drop for QueueWaitGuard<'_> {
58    fn drop(&mut self) {
59        self.counter.fetch_sub(1, Ordering::AcqRel);
60    }
61}
62
63pub fn router(state: Arc<ServerState>) -> Router {
64    let api = Router::new()
65        .route("/kv/get", axum::routing::post(kv::get))
66        .route("/kv/put", axum::routing::post(kv::put))
67        .route("/kv/delete", axum::routing::post(kv::delete))
68        .route("/kv/list", axum::routing::post(kv::list))
69        .route("/kv/search", axum::routing::post(kv::search))
70        .route("/kv/txn/begin", axum::routing::post(kv::txn_begin))
71        .route("/kv/txn/get", axum::routing::post(kv::txn_get))
72        .route("/kv/txn/put", axum::routing::post(kv::txn_put))
73        .route("/kv/txn/delete", axum::routing::post(kv::txn_delete))
74        .route("/kv/txn/commit", axum::routing::post(kv::txn_commit))
75        .route("/kv/txn/rollback", axum::routing::post(kv::txn_rollback))
76        .route("/columnar/scan", axum::routing::post(columnar::scan))
77        .route("/columnar/stats", axum::routing::post(columnar::stats))
78        .route("/columnar/list", axum::routing::post(columnar::list))
79        .route("/columnar/ingest", axum::routing::post(columnar::ingest))
80        .route(
81            "/columnar/index/create",
82            axum::routing::post(columnar::index_create),
83        )
84        .route(
85            "/columnar/index/list",
86            axum::routing::post(columnar::index_list),
87        )
88        .route(
89            "/columnar/index/drop",
90            axum::routing::post(columnar::index_drop),
91        )
92        .route("/hnsw/search", axum::routing::post(hnsw::search))
93        .route("/hnsw/upsert", axum::routing::post(hnsw::upsert))
94        .route("/hnsw/delete", axum::routing::post(hnsw::delete))
95        .route("/hnsw/create", axum::routing::post(hnsw::create))
96        .route("/hnsw/drop", axum::routing::post(hnsw::drop))
97        .route("/hnsw/stats", axum::routing::post(hnsw::stats))
98        .route("/sql", axum::routing::post(sql::handle))
99        .route("/api/sql/query", axum::routing::post(sql::handle))
100        .route("/vector/search", axum::routing::post(vector::search))
101        .route("/vector/upsert", axum::routing::post(vector::upsert))
102        .route("/vector/delete", axum::routing::post(vector::delete))
103        .route(
104            "/vector/index/create",
105            axum::routing::post(vector::index_create),
106        )
107        .route(
108            "/vector/index/update",
109            axum::routing::post(vector::index_update),
110        )
111        .route(
112            "/vector/index/delete",
113            axum::routing::post(vector::index_delete),
114        )
115        .route(
116            "/vector/index/compact",
117            axum::routing::post(vector::index_compact),
118        )
119        .route(
120            "/api/admin/capabilities",
121            axum::routing::get(admin_api::capabilities),
122        )
123        .route(
124            "/api/admin/resources",
125            axum::routing::get(admin_resources::list),
126        )
127        .route("/api/admin/status", axum::routing::get(admin_api::status))
128        .route("/api/admin/metrics", axum::routing::get(admin_api::metrics))
129        .route("/api/admin/health", axum::routing::get(admin_api::health))
130        .route(
131            "/api/admin/cluster/join",
132            axum::routing::post(admin_api::cluster_join),
133        )
134        .route(
135            "/api/admin/cluster/leave",
136            axum::routing::post(admin_api::cluster_leave),
137        )
138        .route(
139            "/api/admin/cluster/metadata",
140            axum::routing::get(admin_api::cluster_metadata),
141        )
142        .route(
143            "/api/admin/cluster/operations",
144            axum::routing::post(admin_api::cluster_management),
145        )
146        .route(
147            "/api/admin/backup",
148            axum::routing::post(admin_api::start_backup),
149        )
150        .route("/api/admin/export", axum::routing::post(admin_api::export))
151        .route(
152            "/api/admin/backup/{id}",
153            axum::routing::get(admin_api::backup_status),
154        )
155        .route(
156            "/api/admin/restore",
157            axum::routing::post(admin_api::start_restore),
158        )
159        .route(
160            "/api/admin/restore/{id}",
161            axum::routing::get(admin_api::restore_status),
162        )
163        .route(
164            "/api/admin/lifecycle",
165            axum::routing::post(admin_api::lifecycle),
166        )
167        .route(
168            "/api/admin/compaction",
169            axum::routing::post(admin_api::compaction),
170        )
171        .route("/session/begin", axum::routing::post(session::begin))
172        .route("/session/{id}/commit", axum::routing::post(session::commit))
173        .route(
174            "/session/{id}/rollback",
175            axum::routing::post(session::rollback),
176        );
177
178    let api = if state.config.api_prefix.is_empty() {
179        api
180    } else {
181        Router::new().nest(&state.config.api_prefix, api)
182    };
183
184    // The distributed-read protocol is versioned independently from the
185    // configurable legacy API prefix. This keeps its documented cancel route
186    // stable even when an installation mounts existing SQL endpoints under a
187    // compatibility prefix.
188    let api = api
189        .route(
190            "/v1/sql/reads",
191            axum::routing::post(sql::begin_distributed_read),
192        )
193        .route(
194            "/v1/sql/reads/{id}",
195            axum::routing::get(sql::stream_distributed_read),
196        )
197        .route(
198            "/v1/sql/reads/{id}/cancel",
199            axum::routing::post(sql::cancel_distributed_read),
200        );
201
202    let middleware = middleware::from_fn(context_middleware);
203    let connection_middleware = middleware::from_fn(connection_middleware);
204    let admission_middleware = middleware::from_fn(admission_middleware);
205    // `RequestBodyLimitLayer` rewraps the request body as `Limited<Body>`, so
206    // it must be the innermost layer (applied last): the `from_fn`
207    // middlewares above are typed against the plain `axum::extract::Request`
208    // (`Request<Body>`) and would not satisfy `Service<Request<Limited<Body>>>`
209    // if body-limiting ran before them (axum 0.7 middleware is no longer
210    // generic over the body type).
211    api.layer(
212        ServiceBuilder::new()
213            .layer(TraceLayer::new_for_http().make_span_with(make_trace_span))
214            .layer(admission_middleware)
215            .layer(middleware)
216            .layer(connection_middleware)
217            .layer(RequestBodyLimitLayer::new(state.config.max_request_size)),
218    )
219    .layer(axum::Extension(state))
220}
221
222pub fn admin_router(state: Arc<ServerState>) -> Router {
223    admin::router(state)
224}
225
226pub async fn context_middleware(
227    axum::extract::Extension(state): axum::extract::Extension<Arc<ServerState>>,
228    mut req: axum::extract::Request,
229    next: middleware::Next,
230) -> Response {
231    let correlation_id =
232        extract_correlation_id(req.headers()).unwrap_or_else(|| Uuid::new_v4().to_string());
233
234    let actor = match state.auth.validate_http(req.headers()) {
235        Ok(actor) => actor,
236        Err(err) => {
237            if state.config.audit_log_enabled {
238                state.audit.log(crate::audit::AuditLogEntry {
239                    event_type: crate::audit::AuditEventType::AuthFailure,
240                    actor: None,
241                    target: "auth".into(),
242                    correlation_id: correlation_id.clone(),
243                    timestamp: chrono::Utc::now(),
244                    details: serde_json::json!({ "error": err.to_string() }),
245                });
246            }
247            return auth_error_response(err, &correlation_id);
248        }
249    };
250
251    req.extensions_mut().insert(RequestContext {
252        correlation_id: correlation_id.clone(),
253        actor,
254    });
255
256    let mut res = next.run(req).await;
257    let _ = res.headers_mut().insert(
258        "x-correlation-id",
259        HeaderValue::from_str(&correlation_id).unwrap_or_else(|_| HeaderValue::from_static("")),
260    );
261    res
262}
263
264pub async fn connection_middleware(
265    axum::extract::Extension(state): axum::extract::Extension<Arc<ServerState>>,
266    req: axum::extract::Request,
267    next: middleware::Next,
268) -> Response {
269    state.metrics.record_connection(1);
270    let res = next.run(req).await;
271    state.metrics.record_connection(-1);
272    res
273}
274
275pub async fn admission_middleware(
276    axum::extract::Extension(state): axum::extract::Extension<Arc<ServerState>>,
277    req: axum::extract::Request,
278    next: middleware::Next,
279) -> Response {
280    if let Ok(permit) = state.admission_permits.clone().try_acquire_owned() {
281        let res = next.run(req).await;
282        drop(permit);
283        return res;
284    }
285
286    let queued_now = state.admission_waiters.fetch_add(1, Ordering::AcqRel) + 1;
287    if queued_now > state.config.max_queue_len {
288        state.admission_waiters.fetch_sub(1, Ordering::AcqRel);
289        let correlation_id =
290            extract_correlation_id(req.headers()).unwrap_or_else(|| Uuid::new_v4().to_string());
291        return queue_overflow_response(&correlation_id);
292    }
293    let queue_wait_guard = QueueWaitGuard::new(&state.admission_waiters);
294
295    let permit = match state.admission_permits.clone().acquire_owned().await {
296        Ok(permit) => permit,
297        Err(_) => {
298            let correlation_id =
299                extract_correlation_id(req.headers()).unwrap_or_else(|| Uuid::new_v4().to_string());
300            return queue_overflow_response(&correlation_id);
301        }
302    };
303    drop(queue_wait_guard);
304
305    let res = next.run(req).await;
306    drop(permit);
307    res
308}
309
310fn auth_error_response(err: AuthError, correlation_id: &str) -> Response {
311    let message = err.to_string();
312    let body = Json(ErrorResponse {
313        error: ErrorBody {
314            code: "UNAUTHORIZED".to_string(),
315            message,
316            correlation_id: correlation_id.to_string(),
317        },
318    });
319    (StatusCode::UNAUTHORIZED, body).into_response()
320}
321
322fn queue_overflow_response(correlation_id: &str) -> Response {
323    let body = Json(ErrorResponse {
324        error: ErrorBody {
325            code: "SERVER_BACKPRESSURE".to_string(),
326            message: "server request queue is full".to_string(),
327            correlation_id: correlation_id.to_string(),
328        },
329    });
330    (StatusCode::SERVICE_UNAVAILABLE, body).into_response()
331}
332
333pub fn error_response(err: ServerError, ctx: &RequestContext) -> Response {
334    let body = Json(ErrorResponse {
335        error: ErrorBody {
336            code: err.error_code(),
337            message: err.to_string(),
338            correlation_id: ctx.correlation_id.clone(),
339        },
340    });
341    (err.status_code(), body).into_response()
342}
343
344fn make_trace_span<B>(request: &axum::http::Request<B>) -> Span {
345    let correlation_id = request
346        .extensions()
347        .get::<RequestContext>()
348        .map(|ctx| ctx.correlation_id.clone())
349        .or_else(|| extract_correlation_id(request.headers()))
350        .unwrap_or_else(|| Uuid::new_v4().to_string());
351    let traceparent = request
352        .headers()
353        .get("traceparent")
354        .and_then(|v| v.to_str().ok())
355        .unwrap_or("");
356    tracing::info_span!(
357        "http_request",
358        correlation_id = %correlation_id,
359        traceparent = %traceparent,
360        method = %request.method(),
361        path = %request.uri().path()
362    )
363}
364
365pub fn json_response<T: Serialize>(value: T, max_size: usize, ctx: &RequestContext) -> Response {
366    match serde_json::to_vec(&value) {
367        Ok(bytes) if bytes.len() <= max_size => (StatusCode::OK, Json(value)).into_response(),
368        Ok(_) => error_response(
369            ServerError::PayloadTooLarge("response size exceeds limit".into()),
370            ctx,
371        ),
372        Err(err) => error_response(ServerError::Internal(err.to_string()), ctx),
373    }
374}
375
376fn extract_correlation_id(headers: &axum::http::HeaderMap) -> Option<String> {
377    headers
378        .get("x-correlation-id")
379        .and_then(|v| v.to_str().ok())
380        .map(|v| v.to_string())
381        .or_else(|| {
382            headers
383                .get("x-request-id")
384                .and_then(|v| v.to_str().ok())
385                .map(|v| v.to_string())
386        })
387}