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