Skip to main content

rivetkit_core/registry/
envoy_callbacks.rs

1use tracing::Instrument;
2
3use super::*;
4use crate::error::ActorRuntime;
5use crate::runtime::RuntimeSpawner;
6
7impl EnvoyCallbacks for RegistryCallbacks {
8	fn on_actor_start(
9		&self,
10		handle: EnvoyHandle,
11		actor_id: String,
12		generation: u32,
13		config: protocol::ActorConfig,
14		_preloaded_kv: Option<protocol::PreloadedKv>,
15	) -> EnvoyBoxFuture<anyhow::Result<()>> {
16		let dispatcher = self.dispatcher.clone();
17		let actor_name = config.name.clone();
18		let key = actor_key_from_protocol(config.key.clone());
19		let input = config.input.clone();
20		let factory = dispatcher.factories.get(&actor_name).cloned();
21
22		Box::pin(async move {
23			let factory = factory.ok_or_else(|| {
24				ActorRuntime::NotRegistered {
25					actor_name: actor_name.clone(),
26				}
27				.build()
28			})?;
29			let ctx = dispatcher.build_actor_context(
30				handle,
31				&actor_id,
32				generation,
33				&actor_name,
34				key,
35				factory.as_ref(),
36			)?;
37
38			dispatcher
39				.start_actor(StartActorRequest {
40					actor_id: actor_id.clone(),
41					generation,
42					actor_name,
43					input,
44					ctx,
45				})
46				.await?;
47
48			Ok(())
49		})
50	}
51
52	fn on_actor_stop_with_completion(
53		&self,
54		_handle: EnvoyHandle,
55		actor_id: String,
56		generation: u32,
57		reason: protocol::StopActorReason,
58		stop_handle: ActorStopHandle,
59	) -> EnvoyBoxFuture<anyhow::Result<()>> {
60		let dispatcher = self.dispatcher.clone();
61		Box::pin(async move {
62			RuntimeSpawner::spawn(
63				async move {
64					if let Err(error) = dispatcher
65						.stop_actor(&actor_id, generation, reason, stop_handle)
66						.await
67					{
68						tracing::error!(
69							?error,
70							"actor stop failed after asynchronous completion handoff",
71						);
72					}
73				}
74				.in_current_span(),
75			);
76			Ok(())
77		})
78	}
79
80	fn on_shutdown(&self) {}
81
82	fn fetch(
83		&self,
84		_handle: EnvoyHandle,
85		actor_id: String,
86		_gateway_id: protocol::GatewayId,
87		_request_id: protocol::RequestId,
88		request: HttpRequest,
89	) -> EnvoyBoxFuture<anyhow::Result<HttpResponse>> {
90		tracing::info!(
91			method = %request.method,
92			path = %request.path,
93			"envoy callback: fetch request"
94		);
95		let dispatcher = self.dispatcher.clone();
96		Box::pin(async move { dispatcher.handle_fetch(&actor_id, request).await })
97	}
98
99	fn websocket(
100		&self,
101		_handle: EnvoyHandle,
102		actor_id: String,
103		_gateway_id: protocol::GatewayId,
104		_request_id: protocol::RequestId,
105		_request: HttpRequest,
106		_path: String,
107		_headers: HashMap<String, String>,
108		_is_hibernatable: bool,
109		is_restoring_hibernatable: bool,
110		sender: WebSocketSender,
111	) -> EnvoyBoxFuture<anyhow::Result<WebSocketHandler>> {
112		tracing::info!(
113			path = %_path,
114			is_hibernatable = _is_hibernatable,
115			is_restoring_hibernatable,
116			"envoy callback: websocket request"
117		);
118		let dispatcher = self.dispatcher.clone();
119		Box::pin(async move {
120			dispatcher
121				.handle_websocket(
122					&actor_id,
123					&_request,
124					&_path,
125					&_headers,
126					&_gateway_id,
127					&_request_id,
128					_is_hibernatable,
129					is_restoring_hibernatable,
130					sender,
131				)
132				.await
133		})
134	}
135
136	fn can_hibernate(
137		&self,
138		actor_id: &str,
139		_gateway_id: &protocol::GatewayId,
140		_request_id: &protocol::RequestId,
141		request: &HttpRequest,
142	) -> EnvoyBoxFuture<anyhow::Result<bool>> {
143		let can_hibernate = self.dispatcher.can_hibernate(actor_id, request);
144		Box::pin(async move { Ok(can_hibernate) })
145	}
146}
147
148impl ServeSettings {
149	fn from_env() -> anyhow::Result<Self> {
150		let engine_host = env::var("RIVET_RUN_ENGINE_HOST").ok();
151		let engine_port = env::var("RIVET_RUN_ENGINE_PORT")
152			.ok()
153			.and_then(|value| value.parse().ok());
154		// Only a configured endpoint goes through auth parsing and URL
155		// validation, mirroring the TypeScript config transform. The built
156		// local default is used verbatim.
157		let parsed = env::var("RIVET_ENDPOINT")
158			.ok()
159			.map(extract_endpoint_auth)
160			.transpose()
161			.context("invalid RIVET_ENDPOINT")?;
162		let env_namespace = env::var("RIVET_NAMESPACE").ok();
163		let env_token = env::var("RIVET_TOKEN").ok();
164		let parsed = parsed.unwrap_or_else(|| ParsedEndpoint {
165			endpoint: default_engine_endpoint(
166				engine_host.as_deref().unwrap_or("127.0.0.1"),
167				engine_port.unwrap_or(6420),
168			),
169			namespace: None,
170			token: None,
171		});
172		if parsed.namespace.is_some() && env_namespace.is_some() {
173			anyhow::bail!(
174				"cannot specify namespace both in the RIVET_ENDPOINT URL and as RIVET_NAMESPACE"
175			);
176		}
177		if parsed.token.is_some() && env_token.is_some() {
178			anyhow::bail!("cannot specify token both in the RIVET_ENDPOINT URL and as RIVET_TOKEN");
179		}
180
181		Ok(Self {
182			version: env::var("RIVET_ENVOY_VERSION")
183				.ok()
184				.and_then(|value| value.parse().ok())
185				.unwrap_or(1),
186			endpoint: parsed.endpoint,
187			token: Some(
188				parsed
189					.token
190					.or(env_token)
191					.unwrap_or_else(|| "dev".to_owned()),
192			),
193			namespace: parsed
194				.namespace
195				.or(env_namespace)
196				.unwrap_or_else(|| "default".to_owned()),
197			pool_name: env::var("RIVET_POOL_NAME").unwrap_or_else(|_| "rivetkit-rust".to_owned()),
198			engine_binary_path: env::var_os("RIVET_ENGINE_BINARY_PATH").map(PathBuf::from),
199			start_services: matches!(env::var("RIVET_RUN_SERVICES").as_deref(), Ok("1")),
200			services_binary_path: env::var_os("RIVET_SERVICES_BINARY").map(PathBuf::from),
201			engine_host,
202			engine_port,
203			engine_spawn: super::EngineSpawnMode::from_env(),
204			engine_auto_download: matches!(
205				env::var("RIVETKIT_ENGINE_AUTO_DOWNLOAD").as_deref(),
206				Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
207			),
208			handle_inspector_http_in_runtime: false,
209			serverless_base_path: None,
210			serverless_package_version: env!("CARGO_PKG_VERSION").to_owned(),
211			serverless_client_endpoint: None,
212			serverless_client_namespace: None,
213			serverless_client_token: None,
214			serverless_validate_endpoint: true,
215			serverless_max_start_payload_bytes: 1_048_576,
216		})
217	}
218}
219
220/// An endpoint with URL auth credentials split out and stripped.
221#[derive(Debug)]
222struct ParsedEndpoint {
223	endpoint: String,
224	namespace: Option<String>,
225	token: Option<String>,
226}
227
228/// Parses an endpoint that may carry `https://namespace:token@host` URL auth,
229/// mirroring the TypeScript `tryParseEndpoint` helper. The returned endpoint
230/// is the URL-normalized string with any credentials stripped. Errors on
231/// invalid URLs, query strings, fragments, and a token without a namespace.
232fn extract_endpoint_auth(endpoint: String) -> anyhow::Result<ParsedEndpoint> {
233	let mut url = url::Url::parse(&endpoint).with_context(|| format!("invalid URL: {endpoint}"))?;
234	if url.query().is_some() {
235		anyhow::bail!("endpoint cannot contain a query string");
236	}
237	if url.fragment().is_some() {
238		anyhow::bail!("endpoint cannot contain a fragment");
239	}
240
241	let namespace = if url.username().is_empty() {
242		None
243	} else {
244		Some(decode_url_auth(url.username())?)
245	};
246	let token = match url.password() {
247		None | Some("") => None,
248		Some(password) => Some(decode_url_auth(password)?),
249	};
250	if token.is_some() && namespace.is_none() {
251		anyhow::bail!("endpoint cannot have a token without a namespace");
252	}
253
254	// Only strip credentials when some were parsed. URLs that carry auth
255	// always have an authority component, so clearing cannot fail there,
256	// while opaque-scheme URLs without auth would reject the setters.
257	if namespace.is_some() || token.is_some() {
258		if url.set_username("").is_err() || url.set_password(None).is_err() {
259			anyhow::bail!("endpoint URL does not support credentials");
260		}
261	}
262
263	Ok(ParsedEndpoint {
264		endpoint: url.to_string(),
265		namespace,
266		token,
267	})
268}
269
270// The raw value is intentionally left out of the error because it may be a
271// token.
272fn decode_url_auth(value: &str) -> anyhow::Result<String> {
273	super::http::percent_decode_path_segment(value)
274		.context("invalid percent-encoding in endpoint URL auth")
275}
276
277fn default_engine_endpoint(host: &str, port: u16) -> String {
278	let url_host = if host.contains(':') && !host.starts_with('[') {
279		format!("[{host}]")
280	} else {
281		host.to_owned()
282	};
283	format!("http://{url_host}:{port}")
284}
285
286impl ServeConfig {
287	pub fn from_env() -> anyhow::Result<Self> {
288		let settings = ServeSettings::from_env()?;
289		Ok(Self {
290			version: settings.version,
291			endpoint: settings.endpoint,
292			token: settings.token,
293			namespace: settings.namespace,
294			pool_name: settings.pool_name,
295			engine_binary_path: settings.engine_binary_path,
296			start_services: settings.start_services,
297			services_binary_path: settings.services_binary_path,
298			engine_host: settings.engine_host,
299			engine_port: settings.engine_port,
300			engine_spawn: settings.engine_spawn,
301			engine_auto_download: settings.engine_auto_download,
302			handle_inspector_http_in_runtime: settings.handle_inspector_http_in_runtime,
303			serverless_base_path: settings.serverless_base_path,
304			serverless_package_version: settings.serverless_package_version,
305			serverless_client_endpoint: settings.serverless_client_endpoint,
306			serverless_client_namespace: settings.serverless_client_namespace,
307			serverless_client_token: settings.serverless_client_token,
308			serverless_validate_endpoint: settings.serverless_validate_endpoint,
309			serverless_max_start_payload_bytes: settings.serverless_max_start_payload_bytes,
310			serverless_cache_envoy: true,
311			..Default::default()
312		})
313	}
314}
315
316fn actor_key_from_protocol(key: Option<String>) -> ActorKey {
317	key.as_deref()
318		.map(deserialize_actor_key_from_protocol)
319		.unwrap_or_default()
320}
321
322fn deserialize_actor_key_from_protocol(key: &str) -> ActorKey {
323	const EMPTY_KEY: &str = "/";
324	const KEY_SEPARATOR: char = '/';
325
326	if key.is_empty() || key == EMPTY_KEY {
327		return Vec::new();
328	}
329
330	let mut parts = Vec::new();
331	let mut current_part = String::new();
332	let mut escaping = false;
333	let mut empty_string_marker = false;
334
335	for ch in key.chars() {
336		if escaping {
337			if ch == '0' {
338				empty_string_marker = true;
339			} else {
340				current_part.push(ch);
341			}
342			escaping = false;
343		} else if ch == '\\' {
344			escaping = true;
345		} else if ch == KEY_SEPARATOR {
346			if empty_string_marker {
347				parts.push(String::new());
348				empty_string_marker = false;
349			} else {
350				parts.push(std::mem::take(&mut current_part));
351			}
352		} else {
353			current_part.push(ch);
354		}
355	}
356
357	if escaping {
358		current_part.push('\\');
359		parts.push(current_part);
360	} else if empty_string_marker {
361		parts.push(String::new());
362	} else if !current_part.is_empty() || !parts.is_empty() {
363		parts.push(current_part);
364	}
365
366	parts.into_iter().map(ActorKeySegment::String).collect()
367}
368
369// Test shim keeps moved tests in crate-root tests/ with private-module access.
370#[cfg(test)]
371#[path = "../../tests/envoy_callbacks.rs"]
372mod tests;