Skip to main content

reifydb_sub_server_http/
handlers.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 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_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}