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	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					.map_err(|e| AppError::BadRequest(format!("JSON encode error: {}", e)))?,
358			})
359			.map_err(|e| AppError::BadRequest(format!("JSON encode error: {}", e)))?;
360			Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_FRAMES.to_string())], body)
361				.into_response())
362		}
363	}
364}
365
366#[instrument(name = "http::extract_identity", level = "trace", skip_all)]
367fn extract_identity(state: &HttpServerState, headers: &HeaderMap) -> Result<IdentityId, AppError> {
368	if let Some(auth_header) = headers.get("authorization") {
369		let auth_str = auth_header.to_str().map_err(|_| AppError::Auth(AuthError::InvalidHeader))?;
370
371		return extract_identity_from_auth_header(state.auth_service(), auth_str).map_err(AppError::Auth);
372	}
373
374	Ok(IdentityId::anonymous())
375}
376
377pub async fn handle_binding(
378	State(state): State<HttpServerState>,
379	Path(path): Path<String>,
380	method: Method,
381	Query(query_params): Query<HashMap<String, String>>,
382	headers: HeaderMap,
383) -> Result<Response, AppError> {
384	let http_method = resolve_http_method(&method)?;
385	let request_path = format!("/{}", path);
386
387	let (binding, path_captures) = resolve_binding(&state, http_method, &method, &request_path)?;
388	let (procedure, namespace) = find_binding_targets(&state, &binding)?;
389	let params = coerce_binding_params(&procedure, &path_captures, &query_params)?;
390
391	let identity = extract_identity(&state, &headers)?;
392	let metadata = build_metadata(&headers);
393
394	let (frames, metrics) =
395		dispatch_binding(&state, namespace.name(), procedure.name(), params, identity, metadata).await?;
396
397	let mut response = encode_binding_response(frames, binding.format)?;
398	insert_meta_headers(response.headers_mut(), &metrics);
399	Ok(response)
400}
401
402#[inline]
403fn resolve_http_method(method: &Method) -> Result<HttpMethod, AppError> {
404	match method.as_str() {
405		"GET" => Ok(HttpMethod::Get),
406		"POST" => Ok(HttpMethod::Post),
407		"PUT" => Ok(HttpMethod::Put),
408		"PATCH" => Ok(HttpMethod::Patch),
409		"DELETE" => Ok(HttpMethod::Delete),
410		_ => Err(AppError::MethodNotAllowed(format!("method `{}` is not supported", method))),
411	}
412}
413
414#[inline]
415fn resolve_binding(
416	state: &HttpServerState,
417	http_method: HttpMethod,
418	method: &Method,
419	request_path: &str,
420) -> Result<(Binding, HashMap<String, String>), AppError> {
421	let bindings = state.engine().catalog().cache().list_http_bindings();
422	let mut any_path_match = false;
423	let mut matched: Option<(Binding, HashMap<String, String>)> = None;
424	for b in &bindings {
425		let BindingProtocol::Http {
426			method: binding_method,
427			path: binding_path,
428		} = &b.protocol
429		else {
430			unreachable!("list_http_bindings returns only HTTP bindings")
431		};
432		if let Some(captures) = match_http_path(binding_path, request_path) {
433			any_path_match = true;
434			if binding_method == &http_method {
435				matched = Some((b.clone(), captures));
436				break;
437			}
438		}
439	}
440	match matched {
441		Some(m) => Ok(m),
442		None if any_path_match => Err(AppError::MethodNotAllowed(format!(
443			"no binding for method `{}` at `{}`",
444			method, request_path
445		))),
446		None => Err(AppError::NotFound(format!("no binding for `{}`", request_path))),
447	}
448}
449
450#[inline]
451fn find_binding_targets(state: &HttpServerState, binding: &Binding) -> Result<(Procedure, Namespace), AppError> {
452	let procedure = state.engine().catalog().cache().find_procedure(binding.procedure_id).ok_or_else(|| {
453		AppError::Internal(format!("binding references missing procedure id {:?}", binding.procedure_id))
454	})?;
455	let namespace = state.engine().catalog().cache().find_namespace(binding.namespace).ok_or_else(|| {
456		AppError::Internal(format!("binding references missing namespace id {:?}", binding.namespace))
457	})?;
458	Ok((procedure, namespace))
459}
460
461#[inline]
462fn coerce_binding_params(
463	procedure: &Procedure,
464	path_captures: &HashMap<String, String>,
465	query_params: &HashMap<String, String>,
466) -> Result<Params, AppError> {
467	let param_names: Vec<&str> = procedure.params().iter().map(|p| p.name.as_str()).collect();
468	for key in query_params.keys() {
469		if !param_names.contains(&key.as_str()) {
470			return Err(AppError::BadRequest(format!("unknown parameter `{}`", key)));
471		}
472		if path_captures.contains_key(key) {
473			return Err(AppError::BadRequest(format!("parameter `{}` given in both path and query", key)));
474		}
475	}
476
477	let mut params: HashMap<String, Value> = HashMap::with_capacity(procedure.params().len());
478	for p in procedure.params() {
479		let raw = match path_captures.get(&p.name).or_else(|| query_params.get(&p.name)) {
480			Some(v) => v,
481			None => {
482				return Err(AppError::BadRequest(format!("missing required parameter `{}`", p.name)));
483			}
484		};
485		let value = coerce_str_to_value(raw, p.param_type.get_type()).map_err(|e| {
486			AppError::BadRequest(format!(
487				"parameter `{}`: cannot coerce `{}` to {:?}: {}",
488				p.name,
489				raw,
490				p.param_type.get_type(),
491				e
492			))
493		})?;
494		params.insert(p.name.clone(), value);
495	}
496	if params.is_empty() {
497		Ok(Params::None)
498	} else {
499		Ok(Params::Named(Arc::new(params)))
500	}
501}
502
503fn insert_meta_headers(headers: &mut HeaderMap, metrics: &ExecutionMetrics) {
504	headers.insert("x-fingerprint", HeaderValue::from_str(&metrics.fingerprint.to_hex()).unwrap());
505	headers.insert("x-duration", HeaderValue::from_str(&metrics.total.to_string()).unwrap());
506}
507
508fn match_http_path(template: &str, request: &str) -> Option<HashMap<String, String>> {
509	let t_segments: Vec<&str> = template.split('/').filter(|s| !s.is_empty()).collect();
510	let r_segments: Vec<&str> = request.split('/').filter(|s| !s.is_empty()).collect();
511	if t_segments.len() != r_segments.len() {
512		return None;
513	}
514	let mut captures = HashMap::new();
515	for (t, r) in t_segments.iter().zip(r_segments.iter()) {
516		if t.starts_with('{') && t.ends_with('}') {
517			let var = &t[1..t.len() - 1];
518			captures.insert(var.to_string(), r.to_string());
519		} else if t != r {
520			return None;
521		}
522	}
523	Some(captures)
524}
525
526fn coerce_str_to_value(s: &str, ty: ValueType) -> Result<Value, String> {
527	match ty {
528		ValueType::Boolean => match s {
529			"true" | "1" => Ok(Value::Boolean(true)),
530			"false" | "0" => Ok(Value::Boolean(false)),
531			_ => Err("expected `true`/`false`".into()),
532		},
533		ValueType::Utf8 => Ok(Value::Utf8(s.to_string())),
534		ValueType::Int1 => s.parse::<i8>().map(Value::Int1).map_err(|e| e.to_string()),
535		ValueType::Int2 => s.parse::<i16>().map(Value::Int2).map_err(|e| e.to_string()),
536		ValueType::Int4 => s.parse::<i32>().map(Value::Int4).map_err(|e| e.to_string()),
537		ValueType::Int8 => s.parse::<i64>().map(Value::Int8).map_err(|e| e.to_string()),
538		ValueType::Int16 => s.parse::<i128>().map(Value::Int16).map_err(|e| e.to_string()),
539		ValueType::Uint1 => s.parse::<u8>().map(Value::Uint1).map_err(|e| e.to_string()),
540		ValueType::Uint2 => s.parse::<u16>().map(Value::Uint2).map_err(|e| e.to_string()),
541		ValueType::Uint4 => s.parse::<u32>().map(Value::Uint4).map_err(|e| e.to_string()),
542		ValueType::Uint8 => s.parse::<u64>().map(Value::Uint8).map_err(|e| e.to_string()),
543		ValueType::Uint16 => s.parse::<u128>().map(Value::Uint16).map_err(|e| e.to_string()),
544		ValueType::Float4 => s
545			.parse::<f32>()
546			.map_err(|e| e.to_string())
547			.and_then(|v| v.try_into().map(Value::Float4).map_err(|_| "invalid f32".to_string())),
548		ValueType::Float8 => s
549			.parse::<f64>()
550			.map_err(|e| e.to_string())
551			.and_then(|v| v.try_into().map(Value::Float8).map_err(|_| "invalid f64".to_string())),
552		other => Err(format!("coercion to {:?} not supported from URL strings", other)),
553	}
554}
555
556fn encode_binding_response(frames: Vec<Frame>, format: BindingFormat) -> Result<Response, AppError> {
557	match format {
558		BindingFormat::Rbcf => match encode_frames_rbcf(&frames) {
559			Ok(bytes) => {
560				Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_RBCF.to_string())], bytes)
561					.into_response())
562			}
563			Err(e) => Err(AppError::BadRequest(format!("RBCF encode error: {}", e))),
564		},
565		BindingFormat::Json => {
566			let resolved = resolve_response_json(frames, false).map_err(AppError::BadRequest)?;
567			Ok((StatusCode::OK, [(header::CONTENT_TYPE, resolved.content_type)], resolved.body)
568				.into_response())
569		}
570		BindingFormat::Frames => Ok(Json(QueryResponse {
571			frames: convert_frames(&frames)
572				.map_err(|e| AppError::BadRequest(format!("JSON encode error: {}", e)))?,
573		})
574		.into_response()),
575	}
576}
577
578#[cfg(test)]
579pub mod tests {
580	use serde_json::from_str;
581
582	use super::*;
583
584	#[test]
585	fn test_match_http_path_static() {
586		assert_eq!(match_http_path("/users", "/users"), Some(HashMap::new()));
587		assert_eq!(match_http_path("/users", "/other"), None);
588	}
589
590	#[test]
591	fn test_match_http_path_capture() {
592		let caps = match_http_path("/users/{id}", "/users/42").unwrap();
593		assert_eq!(caps.get("id"), Some(&"42".to_string()));
594	}
595
596	#[test]
597	fn test_match_http_path_mismatch_length() {
598		assert!(match_http_path("/users/{id}", "/users").is_none());
599		assert!(match_http_path("/users/{id}", "/users/42/extra").is_none());
600	}
601
602	#[test]
603	fn test_coerce_numeric() {
604		assert_eq!(coerce_str_to_value("42", ValueType::Int8).unwrap(), Value::Int8(42));
605		assert!(coerce_str_to_value("xx", ValueType::Int8).is_err());
606	}
607
608	#[test]
609	fn test_coerce_bool() {
610		assert_eq!(coerce_str_to_value("true", ValueType::Boolean).unwrap(), Value::Boolean(true));
611		assert!(coerce_str_to_value("maybe", ValueType::Boolean).is_err());
612	}
613
614	#[test]
615	fn test_statement_request_deserialization() {
616		let json = r#"{"rql": "SELECT 1"}"#;
617		let request: StatementRequest = from_str(json).unwrap();
618		assert_eq!(request.rql, "SELECT 1");
619		assert!(request.params.is_none());
620	}
621
622	#[test]
623	fn test_query_response_serialization() {
624		let response = QueryResponse {
625			frames: Vec::new(),
626		};
627		let json = to_string(&response).unwrap();
628		assert!(json.contains("frames"));
629	}
630
631	#[test]
632	fn test_health_response_serialization() {
633		let response = HealthResponse {
634			status: "ok",
635		};
636		let json = to_string(&response).unwrap();
637		assert_eq!(json, r#"{"status":"ok"}"#);
638	}
639}