1use std::{collections::HashMap, sync::Arc};
5
6use axum::{
7 Json,
8 extract::{Path, Query, State},
9 http::{HeaderMap, HeaderValue, Method, StatusCode, header},
10 response::{IntoResponse, Response},
11};
12use reifydb_codec::json::{to::convert_frames, types::ResponseFrame};
13use reifydb_core::{
14 actors::server::{Operation, ServerAuthResponse, ServerLogoutResponse, ServerMessage},
15 interface::catalog::{
16 binding::{Binding, BindingFormat, BindingProtocol, HttpMethod},
17 namespace::Namespace,
18 procedure::Procedure,
19 },
20 metrics::execution::ExecutionMetrics,
21};
22use reifydb_runtime::actor::reply::reply_channel;
23use reifydb_sub_server::{
24 auth::{AuthError, extract_identity_from_auth_header},
25 binding::dispatch_binding,
26 claim::{WireClaimRequest, native::dispatch_claim},
27 dispatch::dispatch,
28 format::WireFormat,
29 interceptor::{Protocol, RequestContext, RequestMetadata},
30 response::{CONTENT_TYPE_FRAMES, CONTENT_TYPE_RBCF, encode_frames_rbcf, resolve_response_json},
31 wire::WireParams,
32};
33use reifydb_value::{
34 params::Params,
35 value::{Value, frame::frame::Frame, identity::IdentityId, value_type::ValueType},
36};
37use serde::{Deserialize, Serialize};
38use serde_json::to_string;
39use tracing::{debug_span, instrument};
40
41use crate::{error::AppError, state::HttpServerState};
42
43#[derive(Debug, Deserialize)]
44pub struct StatementRequest {
45 pub rql: String,
46
47 #[serde(default)]
48 pub params: Option<WireParams>,
49}
50
51#[derive(Debug, Serialize)]
52pub struct QueryResponse {
53 pub frames: Vec<ResponseFrame>,
54}
55
56#[derive(Debug, Deserialize)]
57pub struct FormatParams {
58 #[serde(default)]
59 pub format: WireFormat,
60 pub unwrap: Option<bool>,
61}
62
63#[derive(Debug, Serialize)]
64pub struct HealthResponse {
65 pub status: &'static str,
66}
67
68pub async fn health() -> impl IntoResponse {
69 (
70 StatusCode::OK,
71 Json(HealthResponse {
72 status: "ok",
73 }),
74 )
75}
76
77#[derive(Debug, Serialize)]
78pub struct LogoutResponse {
79 pub status: String,
80}
81
82#[derive(Debug, Deserialize)]
83pub struct AuthenticateRequest {
84 pub method: String,
85
86 #[serde(default)]
87 pub credentials: HashMap<String, String>,
88}
89
90#[derive(Debug, Serialize)]
91pub struct AuthenticateResponse {
92 pub status: String,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub token: Option<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub identity: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub challenge_id: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub payload: Option<HashMap<String, String>>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub reason: Option<String>,
108}
109
110pub async fn handle_authenticate(
111 State(state): State<HttpServerState>,
112 Json(request): Json<AuthenticateRequest>,
113) -> Result<Response, AppError> {
114 let auth_response = send_authenticate(&state, request).await?;
115
116 match auth_response {
117 ServerAuthResponse::Authenticated {
118 identity,
119 token,
120 } => Ok((
121 StatusCode::OK,
122 Json(AuthenticateResponse {
123 status: "authenticated".to_string(),
124 token: Some(token),
125 identity: Some(identity.to_string()),
126 challenge_id: None,
127 payload: None,
128 reason: None,
129 }),
130 )
131 .into_response()),
132 ServerAuthResponse::Challenge {
133 challenge_id,
134 payload,
135 } => Ok((
136 StatusCode::OK,
137 Json(AuthenticateResponse {
138 status: "challenge".to_string(),
139 token: None,
140 identity: None,
141 challenge_id: Some(challenge_id),
142 payload: Some(payload),
143 reason: None,
144 }),
145 )
146 .into_response()),
147 ServerAuthResponse::Failed {
148 reason,
149 } => Ok((
150 StatusCode::UNAUTHORIZED,
151 Json(AuthenticateResponse {
152 status: "failed".to_string(),
153 token: None,
154 identity: None,
155 challenge_id: None,
156 payload: None,
157 reason: Some(reason),
158 }),
159 )
160 .into_response()),
161 ServerAuthResponse::Error(reason) => Ok((
162 StatusCode::INTERNAL_SERVER_ERROR,
163 Json(AuthenticateResponse {
164 status: "failed".to_string(),
165 token: None,
166 identity: None,
167 challenge_id: None,
168 payload: None,
169 reason: Some(reason),
170 }),
171 )
172 .into_response()),
173 }
174}
175
176#[inline]
177async fn send_authenticate(
178 state: &HttpServerState,
179 request: AuthenticateRequest,
180) -> Result<ServerAuthResponse, AppError> {
181 let (reply, receiver) = reply_channel();
182 let (actor_ref, _handle) = state.spawn_actor();
183 actor_ref
184 .send(ServerMessage::Authenticate {
185 method: request.method,
186 credentials: request.credentials,
187 reply,
188 })
189 .ok()
190 .ok_or_else(|| AppError::Internal("actor mailbox closed".into()))?;
191
192 receiver.recv().await.map_err(|_| AppError::Internal("actor stopped".into()))
193}
194
195pub async fn handle_logout(State(state): State<HttpServerState>, headers: HeaderMap) -> Result<Response, AppError> {
196 let token = extract_bearer_token(&headers)?;
197 let logout_response = send_logout(&state, token).await?;
198
199 match logout_response {
200 ServerLogoutResponse::Ok => Ok((
201 StatusCode::OK,
202 Json(LogoutResponse {
203 status: "ok".to_string(),
204 }),
205 )
206 .into_response()),
207 ServerLogoutResponse::InvalidToken => Err(AppError::Auth(AuthError::InvalidToken)),
208 ServerLogoutResponse::Error(reason) => Err(AppError::Internal(reason)),
209 }
210}
211
212#[inline]
213fn extract_bearer_token(headers: &HeaderMap) -> Result<&str, AppError> {
214 let auth_header = headers.get("authorization").ok_or(AppError::Auth(AuthError::MissingCredentials))?;
215 let auth_str = auth_header.to_str().map_err(|_| AppError::Auth(AuthError::InvalidHeader))?;
216 let token = auth_str.strip_prefix("Bearer ").ok_or(AppError::Auth(AuthError::InvalidHeader))?.trim();
217
218 if token.is_empty() {
219 return Err(AppError::Auth(AuthError::InvalidToken));
220 }
221 Ok(token)
222}
223
224#[inline]
225async fn send_logout(state: &HttpServerState, token: &str) -> Result<ServerLogoutResponse, AppError> {
226 let (reply, receiver) = reply_channel();
227 let (actor_ref, _handle) = state.spawn_actor();
228 actor_ref
229 .send(ServerMessage::Logout {
230 token: token.to_string(),
231 reply,
232 })
233 .ok()
234 .ok_or_else(|| AppError::Internal("actor mailbox closed".into()))?;
235
236 receiver.recv().await.map_err(|_| AppError::Internal("actor stopped".into()))
237}
238
239fn build_metadata(headers: &HeaderMap) -> RequestMetadata {
240 let mut metadata = RequestMetadata::new(Protocol::Http);
241 for (name, value) in headers.iter() {
242 if let Ok(v) = value.to_str() {
243 metadata.insert(name.as_str(), v);
244 }
245 }
246 metadata
247}
248
249#[instrument(name = "http::query", level = "debug", skip_all, fields(format = ?format_params.format))]
250pub async fn handle_query(
251 State(state): State<HttpServerState>,
252 Query(format_params): Query<FormatParams>,
253 headers: HeaderMap,
254 Json(request): Json<StatementRequest>,
255) -> Result<Response, AppError> {
256 execute_and_respond(&state, Operation::Query, &headers, request, &format_params).await
257}
258
259#[instrument(name = "http::admin", level = "debug", skip_all, fields(format = ?format_params.format))]
260pub async fn handle_admin(
261 State(state): State<HttpServerState>,
262 Query(format_params): Query<FormatParams>,
263 headers: HeaderMap,
264 Json(request): Json<StatementRequest>,
265) -> Result<Response, AppError> {
266 execute_and_respond(&state, Operation::Admin, &headers, request, &format_params).await
267}
268
269#[instrument(name = "http::command", level = "debug", skip_all, fields(format = ?format_params.format))]
270pub async fn handle_command(
271 State(state): State<HttpServerState>,
272 Query(format_params): Query<FormatParams>,
273 headers: HeaderMap,
274 Json(request): Json<StatementRequest>,
275) -> Result<Response, AppError> {
276 execute_and_respond(&state, Operation::Command, &headers, request, &format_params).await
277}
278
279#[instrument(name = "http::queue_claim", level = "debug", skip_all)]
280pub async fn handle_queue_claim(
281 State(state): State<HttpServerState>,
282 Query(format_params): Query<FormatParams>,
283 headers: HeaderMap,
284 Json(request): Json<WireClaimRequest>,
285) -> Result<Response, AppError> {
286 let identity = extract_identity(&state, &headers)?;
287 let metadata = build_metadata(&headers);
288 let request = request.into_claim_request().map_err(AppError::BadRequest)?;
289
290 let (frames, metrics) = dispatch_claim(&state, identity, request, metadata).await?;
291
292 let mut response = encode_query_response(frames, &format_params)?;
293 insert_meta_headers(response.headers_mut(), &metrics);
294 Ok(response)
295}
296
297#[instrument(name = "http::execute_and_respond", level = "debug", skip_all, fields(op = ?operation))]
298async fn execute_and_respond(
299 state: &HttpServerState,
300 operation: Operation,
301 headers: &HeaderMap,
302 request: StatementRequest,
303 format_params: &FormatParams,
304) -> Result<Response, AppError> {
305 let ctx = build_request_context(state, operation, headers, request)?;
306
307 let (frames, metrics) = dispatch(state, ctx).await?;
308
309 let mut response = encode_query_response(frames, format_params)?;
310 insert_meta_headers(response.headers_mut(), &metrics);
311 Ok(response)
312}
313
314#[inline]
315fn build_request_context(
316 state: &HttpServerState,
317 operation: Operation,
318 headers: &HeaderMap,
319 request: StatementRequest,
320) -> Result<RequestContext, AppError> {
321 let identity = extract_identity(state, headers)?;
322 let metadata = build_metadata(headers);
323 let params = match request.params {
324 None => Params::None,
325 Some(wp) => wp.into_params().map_err(AppError::InvalidParams)?,
326 };
327 Ok(RequestContext {
328 identity,
329 operation,
330 rql: request.rql,
331 params,
332 metadata,
333 })
334}
335
336#[inline]
337fn encode_query_response(frames: Vec<Frame>, format_params: &FormatParams) -> Result<Response, AppError> {
338 let _encode_span =
339 debug_span!("http::encode", format = ?format_params.format, frame_count = frames.len()).entered();
340 match format_params.format {
341 WireFormat::Rbcf => match encode_frames_rbcf(&frames) {
342 Ok(bytes) => {
343 Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_RBCF.to_string())], bytes)
344 .into_response())
345 }
346 Err(e) => Err(AppError::BadRequest(format!("RBCF encode error: {}", e))),
347 },
348 WireFormat::Json => {
349 let resolved = resolve_response_json(frames, format_params.unwrap.unwrap_or(false))
350 .map_err(AppError::BadRequest)?;
351 Ok((StatusCode::OK, [(header::CONTENT_TYPE, resolved.content_type)], resolved.body)
352 .into_response())
353 }
354 WireFormat::Frames => {
355 let body = to_string(&QueryResponse {
356 frames: convert_frames(&frames),
357 })
358 .map_err(|e| AppError::BadRequest(format!("JSON encode error: {}", e)))?;
359 Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_FRAMES.to_string())], body)
360 .into_response())
361 }
362 }
363}
364
365#[instrument(name = "http::extract_identity", level = "trace", skip_all)]
366fn extract_identity(state: &HttpServerState, headers: &HeaderMap) -> Result<IdentityId, AppError> {
367 if let Some(auth_header) = headers.get("authorization") {
368 let auth_str = auth_header.to_str().map_err(|_| AppError::Auth(AuthError::InvalidHeader))?;
369
370 return extract_identity_from_auth_header(state.auth_service(), auth_str).map_err(AppError::Auth);
371 }
372
373 Ok(IdentityId::anonymous())
374}
375
376pub async fn handle_binding(
377 State(state): State<HttpServerState>,
378 Path(path): Path<String>,
379 method: Method,
380 Query(query_params): Query<HashMap<String, String>>,
381 headers: HeaderMap,
382) -> Result<Response, AppError> {
383 let http_method = resolve_http_method(&method)?;
384 let request_path = format!("/{}", path);
385
386 let (binding, path_captures) = resolve_binding(&state, http_method, &method, &request_path)?;
387 let (procedure, namespace) = find_binding_targets(&state, &binding)?;
388 let params = coerce_binding_params(&procedure, &path_captures, &query_params)?;
389
390 let identity = extract_identity(&state, &headers)?;
391 let metadata = build_metadata(&headers);
392
393 let (frames, metrics) =
394 dispatch_binding(&state, namespace.name(), procedure.name(), params, identity, metadata).await?;
395
396 let mut response = encode_binding_response(frames, binding.format)?;
397 insert_meta_headers(response.headers_mut(), &metrics);
398 Ok(response)
399}
400
401#[inline]
402fn resolve_http_method(method: &Method) -> Result<HttpMethod, AppError> {
403 match method.as_str() {
404 "GET" => Ok(HttpMethod::Get),
405 "POST" => Ok(HttpMethod::Post),
406 "PUT" => Ok(HttpMethod::Put),
407 "PATCH" => Ok(HttpMethod::Patch),
408 "DELETE" => Ok(HttpMethod::Delete),
409 _ => Err(AppError::MethodNotAllowed(format!("method `{}` is not supported", method))),
410 }
411}
412
413#[inline]
414fn resolve_binding(
415 state: &HttpServerState,
416 http_method: HttpMethod,
417 method: &Method,
418 request_path: &str,
419) -> Result<(Binding, HashMap<String, String>), AppError> {
420 let bindings = state.engine().catalog().cache().list_http_bindings();
421 let mut any_path_match = false;
422 let mut matched: Option<(Binding, HashMap<String, String>)> = None;
423 for b in &bindings {
424 let BindingProtocol::Http {
425 method: binding_method,
426 path: binding_path,
427 } = &b.protocol
428 else {
429 unreachable!("list_http_bindings returns only HTTP bindings")
430 };
431 if let Some(captures) = match_http_path(binding_path, request_path) {
432 any_path_match = true;
433 if binding_method == &http_method {
434 matched = Some((b.clone(), captures));
435 break;
436 }
437 }
438 }
439 match matched {
440 Some(m) => Ok(m),
441 None if any_path_match => Err(AppError::MethodNotAllowed(format!(
442 "no binding for method `{}` at `{}`",
443 method, request_path
444 ))),
445 None => Err(AppError::NotFound(format!("no binding for `{}`", request_path))),
446 }
447}
448
449#[inline]
450fn find_binding_targets(state: &HttpServerState, binding: &Binding) -> Result<(Procedure, Namespace), AppError> {
451 let procedure = state.engine().catalog().cache().find_procedure(binding.procedure_id).ok_or_else(|| {
452 AppError::Internal(format!("binding references missing procedure id {:?}", binding.procedure_id))
453 })?;
454 let namespace = state.engine().catalog().cache().find_namespace(binding.namespace).ok_or_else(|| {
455 AppError::Internal(format!("binding references missing namespace id {:?}", binding.namespace))
456 })?;
457 Ok((procedure, namespace))
458}
459
460#[inline]
461fn coerce_binding_params(
462 procedure: &Procedure,
463 path_captures: &HashMap<String, String>,
464 query_params: &HashMap<String, String>,
465) -> Result<Params, AppError> {
466 let param_names: Vec<&str> = procedure.params().iter().map(|p| p.name.as_str()).collect();
467 for key in query_params.keys() {
468 if !param_names.contains(&key.as_str()) {
469 return Err(AppError::BadRequest(format!("unknown parameter `{}`", key)));
470 }
471 if path_captures.contains_key(key) {
472 return Err(AppError::BadRequest(format!("parameter `{}` given in both path and query", key)));
473 }
474 }
475
476 let mut params: HashMap<String, Value> = HashMap::with_capacity(procedure.params().len());
477 for p in procedure.params() {
478 let raw = match path_captures.get(&p.name).or_else(|| query_params.get(&p.name)) {
479 Some(v) => v,
480 None => {
481 return Err(AppError::BadRequest(format!("missing required parameter `{}`", p.name)));
482 }
483 };
484 let value = coerce_str_to_value(raw, p.param_type.get_type()).map_err(|e| {
485 AppError::BadRequest(format!(
486 "parameter `{}`: cannot coerce `{}` to {:?}: {}",
487 p.name,
488 raw,
489 p.param_type.get_type(),
490 e
491 ))
492 })?;
493 params.insert(p.name.clone(), value);
494 }
495 if params.is_empty() {
496 Ok(Params::None)
497 } else {
498 Ok(Params::Named(Arc::new(params)))
499 }
500}
501
502fn insert_meta_headers(headers: &mut HeaderMap, metrics: &ExecutionMetrics) {
503 headers.insert("x-fingerprint", HeaderValue::from_str(&metrics.fingerprint.to_hex()).unwrap());
504 headers.insert("x-duration", HeaderValue::from_str(&metrics.total.to_string()).unwrap());
505}
506
507fn match_http_path(template: &str, request: &str) -> Option<HashMap<String, String>> {
508 let t_segments: Vec<&str> = template.split('/').filter(|s| !s.is_empty()).collect();
509 let r_segments: Vec<&str> = request.split('/').filter(|s| !s.is_empty()).collect();
510 if t_segments.len() != r_segments.len() {
511 return None;
512 }
513 let mut captures = HashMap::new();
514 for (t, r) in t_segments.iter().zip(r_segments.iter()) {
515 if t.starts_with('{') && t.ends_with('}') {
516 let var = &t[1..t.len() - 1];
517 captures.insert(var.to_string(), r.to_string());
518 } else if t != r {
519 return None;
520 }
521 }
522 Some(captures)
523}
524
525fn coerce_str_to_value(s: &str, ty: ValueType) -> Result<Value, String> {
526 match ty {
527 ValueType::Boolean => match s {
528 "true" | "1" => Ok(Value::Boolean(true)),
529 "false" | "0" => Ok(Value::Boolean(false)),
530 _ => Err("expected `true`/`false`".into()),
531 },
532 ValueType::Utf8 => Ok(Value::Utf8(s.to_string())),
533 ValueType::Int1 => s.parse::<i8>().map(Value::Int1).map_err(|e| e.to_string()),
534 ValueType::Int2 => s.parse::<i16>().map(Value::Int2).map_err(|e| e.to_string()),
535 ValueType::Int4 => s.parse::<i32>().map(Value::Int4).map_err(|e| e.to_string()),
536 ValueType::Int8 => s.parse::<i64>().map(Value::Int8).map_err(|e| e.to_string()),
537 ValueType::Int16 => s.parse::<i128>().map(Value::Int16).map_err(|e| e.to_string()),
538 ValueType::Uint1 => s.parse::<u8>().map(Value::Uint1).map_err(|e| e.to_string()),
539 ValueType::Uint2 => s.parse::<u16>().map(Value::Uint2).map_err(|e| e.to_string()),
540 ValueType::Uint4 => s.parse::<u32>().map(Value::Uint4).map_err(|e| e.to_string()),
541 ValueType::Uint8 => s.parse::<u64>().map(Value::Uint8).map_err(|e| e.to_string()),
542 ValueType::Uint16 => s.parse::<u128>().map(Value::Uint16).map_err(|e| e.to_string()),
543 ValueType::Float4 => s
544 .parse::<f32>()
545 .map_err(|e| e.to_string())
546 .and_then(|v| v.try_into().map(Value::Float4).map_err(|_| "invalid f32".to_string())),
547 ValueType::Float8 => s
548 .parse::<f64>()
549 .map_err(|e| e.to_string())
550 .and_then(|v| v.try_into().map(Value::Float8).map_err(|_| "invalid f64".to_string())),
551 other => Err(format!("coercion to {:?} not supported from URL strings", other)),
552 }
553}
554
555fn encode_binding_response(frames: Vec<Frame>, format: BindingFormat) -> Result<Response, AppError> {
556 match format {
557 BindingFormat::Rbcf => match encode_frames_rbcf(&frames) {
558 Ok(bytes) => {
559 Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_RBCF.to_string())], bytes)
560 .into_response())
561 }
562 Err(e) => Err(AppError::BadRequest(format!("RBCF encode error: {}", e))),
563 },
564 BindingFormat::Json => {
565 let resolved = resolve_response_json(frames, false).map_err(AppError::BadRequest)?;
566 Ok((StatusCode::OK, [(header::CONTENT_TYPE, resolved.content_type)], resolved.body)
567 .into_response())
568 }
569 BindingFormat::Frames => Ok(Json(QueryResponse {
570 frames: convert_frames(&frames),
571 })
572 .into_response()),
573 }
574}
575
576#[cfg(test)]
577pub mod tests {
578 use serde_json::from_str;
579
580 use super::*;
581
582 #[test]
583 fn test_match_http_path_static() {
584 assert_eq!(match_http_path("/users", "/users"), Some(HashMap::new()));
585 assert_eq!(match_http_path("/users", "/other"), None);
586 }
587
588 #[test]
589 fn test_match_http_path_capture() {
590 let caps = match_http_path("/users/{id}", "/users/42").unwrap();
591 assert_eq!(caps.get("id"), Some(&"42".to_string()));
592 }
593
594 #[test]
595 fn test_match_http_path_mismatch_length() {
596 assert!(match_http_path("/users/{id}", "/users").is_none());
597 assert!(match_http_path("/users/{id}", "/users/42/extra").is_none());
598 }
599
600 #[test]
601 fn test_coerce_numeric() {
602 assert_eq!(coerce_str_to_value("42", ValueType::Int8).unwrap(), Value::Int8(42));
603 assert!(coerce_str_to_value("xx", ValueType::Int8).is_err());
604 }
605
606 #[test]
607 fn test_coerce_bool() {
608 assert_eq!(coerce_str_to_value("true", ValueType::Boolean).unwrap(), Value::Boolean(true));
609 assert!(coerce_str_to_value("maybe", ValueType::Boolean).is_err());
610 }
611
612 #[test]
613 fn test_statement_request_deserialization() {
614 let json = r#"{"rql": "SELECT 1"}"#;
615 let request: StatementRequest = from_str(json).unwrap();
616 assert_eq!(request.rql, "SELECT 1");
617 assert!(request.params.is_none());
618 }
619
620 #[test]
621 fn test_query_response_serialization() {
622 let response = QueryResponse {
623 frames: Vec::new(),
624 };
625 let json = to_string(&response).unwrap();
626 assert!(json.contains("frames"));
627 }
628
629 #[test]
630 fn test_health_response_serialization() {
631 let response = HealthResponse {
632 status: "ok",
633 };
634 let json = to_string(&response).unwrap();
635 assert_eq!(json, r#"{"status":"ok"}"#);
636 }
637}