Skip to main content

reifydb_sub_server/
interceptor.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};
5
6use futures_util::FutureExt;
7use reifydb_core::{actors::server::Operation, metric::ExecutionMetrics};
8use reifydb_type::{params::Params, value::identity::IdentityId};
9use tracing::error;
10
11use crate::execute::ExecuteError;
12
13/// The transport protocol used for the request.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Protocol {
16	#[default]
17	Http,
18	WebSocket,
19	Grpc,
20}
21
22/// Protocol-agnostic metadata extracted from the request transport layer.
23///
24/// HTTP headers, gRPC metadata, and WS auth tokens are all normalized into
25/// a string-keyed map. Header names are lowercased for consistent lookup.
26///
27/// Note: this is a single-value map - duplicate keys are overwritten
28/// (last-write-wins). Multi-valued headers (e.g. `Set-Cookie`) only
29/// retain the last value. This is intentional for simplicity; most
30/// interceptor use cases only need single-valued lookups.
31#[derive(Debug, Clone, Default)]
32pub struct RequestMetadata {
33	headers: HashMap<String, String>,
34	protocol: Protocol,
35}
36
37impl RequestMetadata {
38	/// Create empty metadata for the given protocol.
39	pub fn new(protocol: Protocol) -> Self {
40		Self {
41			headers: HashMap::new(),
42			protocol,
43		}
44	}
45
46	/// Insert a header (key is lowercased). Duplicate keys are overwritten.
47	pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
48		self.headers.insert(key.into().to_ascii_lowercase(), value.into());
49	}
50
51	/// Get a header value by name (case-insensitive).
52	pub fn get(&self, key: &str) -> Option<&str> {
53		self.headers.get(&key.to_ascii_lowercase()).map(|s| s.as_str())
54	}
55
56	/// Get the protocol.
57	pub fn protocol(&self) -> Protocol {
58		self.protocol
59	}
60
61	/// Get all headers.
62	pub fn headers(&self) -> &HashMap<String, String> {
63		&self.headers
64	}
65}
66
67/// Context available to pre-execute interceptors.
68///
69/// Fields are public and mutable so interceptors can override values
70/// (e.g., resolve API key → set identity, store key_id in metadata for post_execute).
71pub struct RequestContext {
72	/// The resolved identity. Pre-execute interceptors may replace this.
73	pub identity: IdentityId,
74	/// The operation type.
75	pub operation: Operation,
76	/// The RQL string being executed.
77	pub rql: String,
78	/// Query parameters.
79	pub params: Params,
80	/// Protocol-agnostic request metadata (headers, etc.).
81	pub metadata: RequestMetadata,
82}
83
84/// Context available to post-execute interceptors.
85pub struct ResponseContext {
86	/// The identity that executed the request (may have been mutated by pre_execute).
87	pub identity: IdentityId,
88	/// The operation type.
89	pub operation: Operation,
90	/// The RQL string that was executed.
91	pub rql: String,
92	/// Rich metrics for each statement in the request.
93	pub metrics: ExecutionMetrics,
94	/// Query parameters.
95	pub params: Params,
96	/// Protocol-agnostic request metadata.
97	pub metadata: RequestMetadata,
98	/// Execution result: Ok(frame_count) or Err with the error message.
99	pub result: Result<usize, String>,
100}
101
102/// Async trait for request-level interceptors.
103///
104/// Interceptors run in the tokio async context (before compute pool dispatch),
105/// so they can perform async I/O (database lookups, network calls, etc.).
106///
107/// Multiple interceptors are chained: `pre_execute` runs in registration order,
108/// `post_execute` runs in reverse order (like middleware stacks).
109pub trait RequestInterceptor: Send + Sync + 'static {
110	/// Called before query execution.
111	///
112	/// Return `Ok(())` to allow the request to proceed.
113	/// Return `Err(ExecuteError)` to reject the request.
114	/// May mutate the context (e.g., set identity from API key lookup).
115	fn pre_execute<'a>(
116		&'a self,
117		ctx: &'a mut RequestContext,
118	) -> Pin<Box<dyn Future<Output = Result<(), ExecuteError>> + Send + 'a>>;
119
120	/// Called after query execution completes (success or failure).
121	///
122	/// This is called even if the execution failed, so interceptors can
123	/// log failures and track usage regardless of outcome.
124	fn post_execute<'a>(&'a self, ctx: &'a ResponseContext) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
125}
126
127/// Ordered chain of request interceptors, cheap to clone (Arc internally).
128#[derive(Clone)]
129pub struct RequestInterceptorChain {
130	interceptors: Arc<Vec<Arc<dyn RequestInterceptor>>>,
131}
132
133impl RequestInterceptorChain {
134	pub fn new(interceptors: Vec<Arc<dyn RequestInterceptor>>) -> Self {
135		Self {
136			interceptors: Arc::new(interceptors),
137		}
138	}
139
140	pub fn empty() -> Self {
141		Self {
142			interceptors: Arc::new(Vec::new()),
143		}
144	}
145
146	pub fn is_empty(&self) -> bool {
147		self.interceptors.is_empty()
148	}
149
150	/// Run all pre_execute interceptors in order.
151	/// Stops and returns Err on first rejection.
152	pub async fn pre_execute(&self, ctx: &mut RequestContext) -> Result<(), ExecuteError> {
153		for interceptor in self.interceptors.iter() {
154			interceptor.pre_execute(ctx).await?;
155		}
156		Ok(())
157	}
158
159	/// Run all post_execute interceptors in reverse order.
160	///
161	/// If an interceptor panics, the panic is caught and logged so that
162	/// remaining interceptors still run.
163	pub async fn post_execute(&self, ctx: &ResponseContext) {
164		for interceptor in self.interceptors.iter().rev() {
165			if let Err(panic) = AssertUnwindSafe(interceptor.post_execute(ctx)).catch_unwind().await {
166				let msg = panic
167					.downcast_ref::<&str>()
168					.copied()
169					.or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
170					.unwrap_or("unknown panic");
171				error!("post_execute interceptor panicked: {}", msg);
172			}
173		}
174	}
175}
176
177impl Default for RequestInterceptorChain {
178	fn default() -> Self {
179		Self::empty()
180	}
181}