Skip to main content

reifydb_sub_server_http/
handlers.rs

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