1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use anyhow::{Context, Result};
7use http::StatusCode;
8use rivet_envoy_client::config::{ActorName as EnvoyActorName, EnvoyConfig};
9use rivet_envoy_client::envoy::start_envoy_sync;
10use rivet_envoy_client::handle::EnvoyHandle;
11use rivet_envoy_client::protocol;
12use rivetkit_shared_types::serverless_metadata::{
13 ActorName, ServerlessMetadataEnvoy, ServerlessMetadataEnvoyKind, ServerlessMetadataPayload,
14};
15use serde::Serialize;
16use serde_json::json;
17use tokio::sync::{Mutex as TokioMutex, mpsc};
18use tokio_util::sync::CancellationToken;
19use url::Url;
20
21use crate::actor::factory::ActorFactory;
22#[cfg(feature = "native-runtime")]
23use crate::development_process::DevelopmentProcessManager;
24use crate::registry::{
25 CoreEnvoyHandle, CoreEnvoyStatus, RegistryCallbacks, RegistryDispatcher, ServeConfig,
26 should_manage_engine,
27};
28use crate::runtime::RuntimeSpawner;
29use crate::time::{Instant, sleep, timeout};
30
31const DEFAULT_BASE_PATH: &str = "/api/rivet";
32const SSE_PING_INTERVAL: Duration = Duration::from_secs(1);
33const SSE_PING_FRAME: &[u8] = b"event: ping\ndata:\n\n";
34const SSE_STOPPING_FRAME: &[u8] = b"event: stopping\ndata:\n\n";
35const SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(20);
39const ENVOY_START_TIMEOUT: Duration = Duration::from_secs(30);
42
43#[derive(Clone)]
44pub struct CoreServerlessRuntime {
45 settings: Arc<ServerlessSettings>,
46 dispatcher: Arc<RegistryDispatcher>,
47 envoy: Arc<TokioMutex<Option<CachedEnvoy>>>,
48 #[cfg(feature = "native-runtime")]
49 development_processes: Arc<TokioMutex<Option<DevelopmentProcessManager>>>,
50 shutting_down: Arc<AtomicBool>,
51}
52
53#[derive(Clone)]
59struct CachedEnvoy {
60 handle: EnvoyHandle,
61 start_deadline: Instant,
62}
63
64#[derive(Clone, Debug)]
65struct ServerlessSettings {
66 version: u32,
67 configured_endpoint: String,
68 configured_namespace: String,
69 base_path: String,
70 package_version: String,
71 client_endpoint: Option<String>,
72 client_namespace: Option<String>,
73 client_token: Option<String>,
74 validate_endpoint: bool,
75 max_start_payload_bytes: usize,
76 cache_envoy: bool,
77}
78
79#[derive(Debug)]
80pub struct ServerlessRequest {
81 pub method: String,
82 pub url: String,
83 pub headers: HashMap<String, String>,
84 pub body: Vec<u8>,
85 pub cancel_token: CancellationToken,
86}
87
88#[derive(Debug)]
89pub struct ServerlessResponse {
90 pub status: u16,
91 pub headers: HashMap<String, String>,
92 pub body: mpsc::UnboundedReceiver<Result<Vec<u8>, ServerlessStreamError>>,
93}
94
95#[derive(Clone, Debug, Serialize)]
96pub struct ServerlessStreamError {
97 pub group: String,
98 pub code: String,
99 pub message: String,
100}
101
102#[derive(Debug)]
103struct StartHeaders {
104 endpoint: String,
105 token: Option<String>,
106 pool_name: String,
107 namespace: String,
108}
109
110#[derive(Debug, Serialize)]
111struct ServerlessErrorBody<'a> {
112 group: &'a str,
113 code: &'a str,
114 message: String,
115 metadata: serde_json::Value,
116}
117
118#[derive(rivet_error::RivetError, Serialize)]
119#[error("request", "invalid", "Invalid request.", "Invalid request: {reason}")]
120struct InvalidRequest {
121 reason: String,
122}
123
124#[derive(rivet_error::RivetError, Serialize)]
125#[error(
126 "config",
127 "endpoint_mismatch",
128 "Endpoint mismatch.",
129 "Endpoint mismatch: expected \"{expected}\", received \"{received}\""
130)]
131struct EndpointMismatch {
132 expected: String,
133 received: String,
134}
135
136#[derive(rivet_error::RivetError, Serialize)]
137#[error(
138 "config",
139 "namespace_mismatch",
140 "Namespace mismatch.",
141 "Namespace mismatch: expected \"{expected}\", received \"{received}\""
142)]
143struct NamespaceMismatch {
144 expected: String,
145 received: String,
146}
147
148#[derive(rivet_error::RivetError, Serialize)]
149#[error(
150 "message",
151 "incoming_too_long",
152 "Incoming message too long.",
153 "Incoming message too long. Exceeded limit of {limit} bytes."
154)]
155struct IncomingMessageTooLong {
156 limit: usize,
157}
158
159#[derive(rivet_error::RivetError, Serialize)]
160#[error(
161 "registry",
162 "shut_down",
163 "Registry is shut down.",
164 "Registry is shut down; no new requests can be accepted."
165)]
166struct RuntimeShutDown;
167
168#[derive(rivet_error::RivetError, Serialize)]
169#[error(
170 "registry",
171 "envoy_start_failed",
172 "Envoy failed to connect to the engine.",
173 "Envoy failed to connect to the engine: {reason}"
174)]
175struct EnvoyStartFailed {
176 reason: String,
177}
178
179impl CoreServerlessRuntime {
180 pub(crate) async fn new(
181 factories: HashMap<String, Arc<ActorFactory>>,
182 config: ServeConfig,
183 ) -> Result<Self> {
184 #[cfg(feature = "sqlite-local")]
185 sqlite_embedded::vfs::verify_batch_atomic_writes_at_process_startup().await?;
186
187 let manage_engine = should_manage_engine(config.engine_spawn);
188 #[cfg(feature = "native-runtime")]
189 let development_processes = if manage_engine {
190 Some(DevelopmentProcessManager::start(&config).await?)
191 } else {
192 None
193 };
194 #[cfg(not(feature = "native-runtime"))]
195 if manage_engine {
196 anyhow::bail!("engine process spawning requires the `native-runtime` feature");
197 }
198
199 let dispatcher = Arc::new(RegistryDispatcher::new(
200 factories,
201 config.handle_inspector_http_in_runtime,
202 ));
203 let base_path = normalize_base_path(config.serverless_base_path.as_deref());
204 crate::metrics::record_rivetkit_info(
205 config.serverless_package_version.clone(),
206 config.version,
207 "serverless",
208 config.pool_name.clone(),
209 );
210 #[cfg(not(target_arch = "wasm32"))]
211 crate::tokio_runtime_metrics::ensure_sampler_started();
212
213 Ok(Self {
214 settings: Arc::new(ServerlessSettings {
215 version: config.version,
216 configured_endpoint: config.endpoint,
217 configured_namespace: config.namespace,
218 base_path,
219 package_version: config.serverless_package_version,
220 client_endpoint: config.serverless_client_endpoint,
221 client_namespace: config.serverless_client_namespace,
222 client_token: config.serverless_client_token,
223 validate_endpoint: config.serverless_validate_endpoint,
224 max_start_payload_bytes: config.serverless_max_start_payload_bytes,
225 cache_envoy: config.serverless_cache_envoy,
226 }),
227 dispatcher,
228 envoy: Arc::new(TokioMutex::new(None)),
229 #[cfg(feature = "native-runtime")]
230 development_processes: Arc::new(TokioMutex::new(development_processes)),
231 shutting_down: Arc::new(AtomicBool::new(false)),
232 })
233 }
234
235 pub async fn shutdown(&self) {
242 self.shutting_down.store(true, Ordering::Release);
243 let handle = { self.envoy.lock().await.take() }.map(|cached| cached.handle);
244 #[cfg(feature = "native-runtime")]
245 let development_processes = { self.development_processes.lock().await.take() };
246 let shutdown_envoy = async move {
247 if let Some(handle) = handle {
248 match timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.shutdown_and_wait(false)).await {
249 Ok(()) => {}
250 Err(_) => {
251 tracing::warn!(
252 "serverless runtime envoy drain exceeded timeout; forcing immediate stop"
253 );
254 handle.shutdown(true);
255 handle.wait_stopped().await;
256 }
257 }
258 }
259 };
260 #[cfg(feature = "native-runtime")]
261 let shutdown_development_processes = async move {
262 if let Some(development_processes) = development_processes {
263 development_processes.shutdown().await;
264 }
265 };
266 #[cfg(feature = "native-runtime")]
267 tokio::join!(shutdown_envoy, shutdown_development_processes);
268 #[cfg(not(feature = "native-runtime"))]
269 shutdown_envoy.await;
270 }
271
272 pub async fn wait_actors_drained(&self, timeout_dur: Duration) {
276 let handle = { self.cached_envoy_handle().await };
277 let Some(handle) = handle else { return };
278 let _ = timeout(
279 timeout_dur,
280 CoreEnvoyHandle::new(handle).wait_actors_drained(),
281 )
282 .await;
283 }
284
285 pub async fn active_envoy_actor_count(&self) -> Option<usize> {
286 self.active_envoy_status()
287 .await
288 .map(|status| status.active_actor_count)
289 }
290
291 pub async fn active_envoy_status(&self) -> Option<CoreEnvoyStatus> {
292 self.cached_envoy_handle()
293 .await
294 .map(|handle| CoreEnvoyHandle::new(handle).status())
295 }
296
297 pub async fn active_envoy_actor_stop_threshold_ms(&self) -> Option<i64> {
298 let handle = self.cached_envoy_handle().await?;
299 CoreEnvoyHandle::new(handle).actor_stop_threshold_ms().await
300 }
301
302 async fn cached_envoy_handle(&self) -> Option<EnvoyHandle> {
303 self.envoy
304 .lock()
305 .await
306 .as_ref()
307 .map(|cached| cached.handle.clone())
308 }
309
310 pub fn max_request_body_bytes(&self) -> usize {
312 self.settings.max_start_payload_bytes
313 }
314
315 pub fn handles_listener_request(&self, url: &str) -> bool {
319 handles_listener_request(&self.settings.base_path, url)
320 }
321
322 pub fn incoming_too_long_response(&self) -> ServerlessResponse {
324 let error = IncomingMessageTooLong {
325 limit: self.settings.max_start_payload_bytes,
326 }
327 .build();
328 error_response(error)
329 }
330
331 pub fn invalid_request_response(&self, reason: impl Into<String>) -> ServerlessResponse {
333 let error = InvalidRequest {
334 reason: reason.into(),
335 }
336 .build();
337 error_response(error)
338 }
339
340 pub async fn handle_request(&self, req: ServerlessRequest) -> ServerlessResponse {
341 let cors = cors_headers(&req);
342 match self.handle_request_inner(req).await {
343 Ok(mut response) => {
344 apply_cors(&mut response.headers, cors);
345 response
346 }
347 Err(error) => {
348 let mut response = error_response(error);
349 apply_cors(&mut response.headers, cors);
350 response
351 }
352 }
353 }
354
355 async fn handle_request_inner(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
356 let path = route_path(&self.settings.base_path, &req.url)?;
357 match (req.method.as_str(), path.as_str()) {
358 ("GET", "") | ("GET", "/") => Ok(text_response(
359 StatusCode::OK,
360 "text/plain; charset=utf-8",
361 "This is a RivetKit server.\n\nLearn more at https://rivet.dev",
362 )),
363 ("GET", "/health") => {
364 let handle = { self.cached_envoy_handle().await };
372 let status = match handle {
373 Some(handle) if handle.terminal_error().is_some() => "registration_rejected",
374 Some(handle) if handle.has_started() && !handle.is_ping_healthy() => {
375 "engine_ping_stale"
376 }
377 _ => "ok",
378 };
379 if status == "ok" {
380 Ok(json_response(
381 StatusCode::OK,
382 json!({
383 "status": "ok",
384 "runtime": "rivetkit",
385 "version": self.settings.package_version,
386 }),
387 ))
388 } else {
389 Ok(json_response(
390 StatusCode::SERVICE_UNAVAILABLE,
391 json!({
392 "status": status,
393 "runtime": "rivetkit",
394 "version": self.settings.package_version,
395 }),
396 ))
397 }
398 }
399 ("GET", "/metadata") => Ok(self.metadata_response()),
400 ("GET", "/start") | ("POST", "/start") => self.start_response(req).await,
401 ("OPTIONS", _) => Ok(bytes_response(
402 StatusCode::NO_CONTENT,
403 HashMap::new(),
404 Vec::new(),
405 )),
406 _ => Ok(text_response(
407 StatusCode::NOT_FOUND,
408 "text/plain; charset=utf-8",
409 "Not Found (RivetKit)",
410 )),
411 }
412 }
413
414 async fn start_response(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
415 let headers = parse_start_headers(&req.headers)?;
416 self.validate_start_headers(&headers)?;
417 crate::metrics::record_rivetkit_info(
418 self.settings.package_version.clone(),
419 self.settings.version,
420 "serverless",
421 headers.pool_name.clone(),
422 );
423 #[cfg(not(target_arch = "wasm32"))]
424 crate::tokio_runtime_metrics::ensure_sampler_started();
425 if req.body.len() > self.settings.max_start_payload_bytes {
426 return Err(IncomingMessageTooLong {
427 limit: self.settings.max_start_payload_bytes,
428 }
429 .build());
430 }
431
432 let handle = self.ensure_envoy(&headers).await?;
433 let payload = req.body;
434 let actor_start = handle.decode_serverless_actor_start(&payload)?;
435 let cancel_token = req.cancel_token;
436 let cache_envoy = self.settings.cache_envoy;
437 let (tx, rx) = mpsc::unbounded_channel();
438 let _ = tx.send(Ok(SSE_PING_FRAME.to_vec()));
439
440 RuntimeSpawner::spawn(async move {
441 let shutdown_handle = handle.clone();
442 let result = tokio::select! {
443 _ = cancel_token.cancelled() => {
444 if !cache_envoy {
445 shutdown_handle.shutdown_and_wait(false).await;
446 }
447 return;
448 }
449 result = handle.start_serverless_actor(&payload) => result,
450 };
451 if let Err(error) = result {
452 let error = stream_error(error);
453 let _ = tx.send(Err(error));
454 if !cache_envoy {
455 handle.shutdown_and_wait(false).await;
456 }
457 return;
458 }
459
460 loop {
461 tokio::select! {
462 _ = cancel_token.cancelled() => {
463 break;
464 }
465 _ = handle.wait_actor_registered_then_stopped(&actor_start.actor_id, actor_start.generation) => {
466 let _ = tx.send(Ok(SSE_STOPPING_FRAME.to_vec()));
467 break;
468 }
469 _ = sleep(SSE_PING_INTERVAL) => {
470 if tx.send(Ok(SSE_PING_FRAME.to_vec())).is_err() {
471 break;
472 }
473 }
474 }
475 }
476
477 if !cache_envoy {
478 handle.shutdown_and_wait(false).await;
479 }
480 });
481
482 Ok(ServerlessResponse {
483 status: StatusCode::OK.as_u16(),
484 headers: HashMap::from([
485 ("content-type".to_owned(), "text/event-stream".to_owned()),
486 ("cache-control".to_owned(), "no-cache".to_owned()),
487 ("connection".to_owned(), "keep-alive".to_owned()),
488 ]),
489 body: rx,
490 })
491 }
492
493 fn metadata_response(&self) -> ServerlessResponse {
494 let actor_names = self
495 .dispatcher
496 .build_actor_metadata_map()
497 .into_iter()
498 .map(|(name, metadata)| {
499 (
500 name,
501 ActorName {
502 metadata: Some(metadata),
503 },
504 )
505 })
506 .collect::<HashMap<_, _>>();
507
508 let payload = ServerlessMetadataPayload {
509 runtime: "rivetkit".to_owned(),
510 version: self.settings.package_version.clone(),
511 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
512 actor_names,
513 envoy: Some(ServerlessMetadataEnvoy {
514 kind: Some(ServerlessMetadataEnvoyKind::Serverless {}),
515 version: Some(self.settings.version),
516 }),
517 runner: None,
518 client_endpoint: self.settings.client_endpoint.clone(),
519 client_namespace: self.settings.client_namespace.clone(),
520 client_token: self.settings.client_token.clone(),
521 };
522
523 let response = serde_json::to_value(payload).unwrap_or_else(|_| json!({}));
524
525 json_response(StatusCode::OK, response)
526 }
527
528 fn validate_start_headers(&self, headers: &StartHeaders) -> Result<()> {
529 if self.settings.validate_endpoint {
530 if !endpoints_match(&headers.endpoint, &self.settings.configured_endpoint) {
531 tracing::warn!(
532 configured_endpoint = %self.settings.configured_endpoint,
533 received_endpoint = %headers.endpoint,
534 "serverless start rejected: endpoint mismatch",
535 );
536 return Err(EndpointMismatch {
537 expected: self.settings.configured_endpoint.clone(),
538 received: headers.endpoint.clone(),
539 }
540 .build());
541 }
542
543 if headers.namespace != self.settings.configured_namespace {
544 tracing::warn!(
545 configured_namespace = %self.settings.configured_namespace,
546 received_namespace = %headers.namespace,
547 "serverless start rejected: namespace mismatch",
548 );
549 return Err(NamespaceMismatch {
550 expected: self.settings.configured_namespace.clone(),
551 received: headers.namespace.clone(),
552 }
553 .build());
554 }
555 }
556
557 Ok(())
558 }
559
560 async fn ensure_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
566 if self.shutting_down.load(Ordering::Acquire) {
567 return Err(RuntimeShutDown.build());
568 }
569 if !self.settings.cache_envoy {
570 let handle = start_envoy_sync(self.envoy_config(headers));
571 let start_deadline = Instant::now() + ENVOY_START_TIMEOUT;
572 if let Err(reason) = wait_for_envoy_start(&handle, start_deadline).await {
573 return Err(self.stop_failed_envoy(&handle, reason).await);
574 }
575 return Ok(handle);
576 }
577
578 let cached = {
579 let mut guard = self.envoy.lock().await;
580 if self.shutting_down.load(Ordering::Acquire) {
584 return Err(RuntimeShutDown.build());
585 }
586 match guard.as_ref() {
587 Some(cached) => {
588 let handle = &cached.handle;
592 if !endpoints_match(handle.endpoint(), &headers.endpoint)
593 || handle.namespace() != headers.namespace
594 || handle.pool_name() != headers.pool_name
595 {
596 anyhow::bail!("serverless start headers do not match active envoy");
597 }
598 cached.clone()
599 }
600 None => {
601 let cached = CachedEnvoy {
602 handle: start_envoy_sync(self.envoy_config(headers)),
603 start_deadline: Instant::now() + ENVOY_START_TIMEOUT,
604 };
605 *guard = Some(cached.clone());
606 cached
607 }
608 }
609 };
610
611 if let Err(reason) = wait_for_envoy_start(&cached.handle, cached.start_deadline).await {
612 if let Some(error) = cached.handle.terminal_error() {
615 return Err(anyhow::anyhow!(error));
616 }
617 {
620 let mut guard = self.envoy.lock().await;
621 let is_cached = guard.as_ref().is_some_and(|current| {
622 current.handle.get_envoy_key() == cached.handle.get_envoy_key()
623 });
624 if is_cached {
625 *guard = None;
626 }
627 }
628 return Err(self.stop_failed_envoy(&cached.handle, reason).await);
629 }
630 Ok(cached.handle)
631 }
632
633 async fn stop_failed_envoy(&self, handle: &EnvoyHandle, reason: String) -> anyhow::Error {
636 handle.shutdown(true);
637 if timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.wait_stopped())
638 .await
639 .is_err()
640 {
641 tracing::warn!(
642 "serverless envoy did not stop after a failed start; the engine may be unreachable"
643 );
644 }
645
646 if self.shutting_down.load(Ordering::Acquire) {
647 return RuntimeShutDown.build();
648 }
649 if let Some(error) = handle.terminal_error() {
650 return anyhow::anyhow!(error);
651 }
652 tracing::warn!(%reason, "serverless envoy failed to start; check engine reachability");
653 EnvoyStartFailed { reason }.build()
654 }
655
656 fn envoy_config(&self, headers: &StartHeaders) -> EnvoyConfig {
657 let callbacks = Arc::new(RegistryCallbacks {
658 dispatcher: self.dispatcher.clone(),
659 });
660 let prepopulate_actor_names = self
661 .dispatcher
662 .build_actor_metadata_map()
663 .into_iter()
664 .map(|(name, metadata)| (name, EnvoyActorName { metadata }))
665 .collect();
666 EnvoyConfig {
671 version: self.settings.version,
672 endpoint: headers.endpoint.clone(),
673 token: headers.token.clone(),
674 namespace: headers.namespace.clone(),
675 pool_name: headers.pool_name.clone(),
676 prepopulate_actor_names,
677 metadata: Some(json!({
678 "rivetkit": { "version": self.settings.package_version },
679 })),
680 not_global: true,
681 debug_latency_ms: None,
682 max_queued_requests_per_actor:
683 rivet_envoy_client::config::DEFAULT_MAX_QUEUED_REQUESTS_PER_ACTOR,
684 stopped_generation_floor_ttl:
685 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_TTL,
686 stopped_generation_floor_capacity:
687 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_CAPACITY,
688 callbacks,
689 connector: None,
690 }
691 }
692}
693
694fn route_path(base_path: &str, url: &str) -> Result<String> {
695 let parsed = Url::parse(url).with_context(|| format!("parse request URL `{url}`"))?;
696 let path = parsed.path();
697 if path == base_path {
698 return Ok(String::new());
699 }
700 let prefix = format!("{base_path}/");
701 if let Some(rest) = path.strip_prefix(&prefix) {
702 return Ok(format!("/{rest}"));
703 }
704 Ok(path.to_owned())
705}
706
707fn handles_listener_request(base_path: &str, url: &str) -> bool {
708 let Ok(parsed) = Url::parse(url) else {
709 return true;
712 };
713 let request_path = parsed.path();
714 if request_path != base_path && !request_path.starts_with(&format!("{base_path}/")) {
715 return false;
716 }
717 let path = route_path(base_path, url).expect("URL was parsed and the same base path is valid");
718 matches!(path.as_str(), "" | "/" | "/health" | "/metadata" | "/start")
719}
720
721fn parse_start_headers(headers: &HashMap<String, String>) -> Result<StartHeaders> {
722 let pool_name = match optional_header(headers, "x-rivet-pool-name") {
723 Some(pool_name) => pool_name,
724 None => optional_header(headers, "x-rivet-runner-name").ok_or_else(|| {
725 InvalidRequest {
726 reason: "x-rivet-pool-name header is required".to_string(),
727 }
728 .build()
729 })?,
730 };
731
732 Ok(StartHeaders {
733 endpoint: required_header(headers, "x-rivet-endpoint")?,
734 token: optional_header(headers, "x-rivet-token"),
735 pool_name,
736 namespace: required_header(headers, "x-rivet-namespace-name")?,
737 })
738}
739
740fn required_header(headers: &HashMap<String, String>, name: &str) -> Result<String> {
741 headers
742 .get(name)
743 .filter(|value| !value.is_empty())
744 .cloned()
745 .ok_or_else(|| {
746 InvalidRequest {
747 reason: format!("{name} header is required"),
748 }
749 .build()
750 })
751}
752
753fn optional_header(headers: &HashMap<String, String>, name: &str) -> Option<String> {
754 headers.get(name).filter(|value| !value.is_empty()).cloned()
755}
756
757fn cors_headers(req: &ServerlessRequest) -> HashMap<String, String> {
758 let origin = req
759 .headers
760 .get("origin")
761 .cloned()
762 .unwrap_or_else(|| "*".to_owned());
763 let mut headers = HashMap::from([
764 ("access-control-allow-origin".to_owned(), origin.clone()),
765 (
766 "access-control-allow-credentials".to_owned(),
767 "true".to_owned(),
768 ),
769 ("access-control-expose-headers".to_owned(), "*".to_owned()),
770 ]);
771 if origin != "*" {
772 headers.insert("vary".to_owned(), "Origin".to_owned());
773 }
774
775 if req.method == "OPTIONS" {
776 headers.insert(
777 "access-control-allow-methods".to_owned(),
778 "GET, POST, PUT, DELETE, OPTIONS, PATCH".to_owned(),
779 );
780 headers.insert(
781 "access-control-allow-headers".to_owned(),
782 req.headers
783 .get("access-control-request-headers")
784 .cloned()
785 .unwrap_or_else(|| "*".to_owned()),
786 );
787 headers.insert("access-control-max-age".to_owned(), "86400".to_owned());
788 }
789
790 headers
791}
792
793fn apply_cors(headers: &mut HashMap<String, String>, cors: HashMap<String, String>) {
794 headers.extend(cors);
795}
796
797fn normalize_base_path(base_path: Option<&str>) -> String {
798 let base_path = base_path
799 .filter(|base_path| !base_path.is_empty())
800 .unwrap_or(DEFAULT_BASE_PATH);
801 let prefixed = if base_path.starts_with('/') {
802 base_path.to_owned()
803 } else {
804 format!("/{base_path}")
805 };
806 let trimmed = prefixed.trim_end_matches('/');
807 if trimmed.is_empty() {
808 "/".to_owned()
809 } else {
810 trimmed.to_owned()
811 }
812}
813
814fn text_response(status: StatusCode, content_type: &str, body: &str) -> ServerlessResponse {
815 bytes_response(
816 status,
817 HashMap::from([("content-type".to_owned(), content_type.to_owned())]),
818 body.as_bytes().to_vec(),
819 )
820}
821
822fn json_response(status: StatusCode, body: serde_json::Value) -> ServerlessResponse {
823 bytes_response(
824 status,
825 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
826 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
827 )
828}
829
830fn bytes_response(
831 status: StatusCode,
832 headers: HashMap<String, String>,
833 body: Vec<u8>,
834) -> ServerlessResponse {
835 let (tx, rx) = mpsc::unbounded_channel();
836 let _ = tx.send(Ok(body));
837 ServerlessResponse {
838 status: status.as_u16(),
839 headers,
840 body: rx,
841 }
842}
843
844fn error_response(error: anyhow::Error) -> ServerlessResponse {
845 let extracted = rivet_error::RivetError::extract(&error);
846 let status = serverless_error_status(extracted.group(), extracted.code());
847 let body = ServerlessErrorBody {
848 group: extracted.group(),
849 code: extracted.code(),
850 message: extracted.message().to_owned(),
851 metadata: extracted.metadata().unwrap_or(serde_json::Value::Null),
852 };
853 bytes_response(
854 status,
855 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
856 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
857 )
858}
859
860fn serverless_error_status(group: &str, code: &str) -> StatusCode {
861 match (group, code) {
862 ("auth", "forbidden") => StatusCode::FORBIDDEN,
863 ("message", "incoming_too_long") => StatusCode::PAYLOAD_TOO_LARGE,
864 ("registry", "envoy_start_failed") => StatusCode::SERVICE_UNAVAILABLE,
865 _ => StatusCode::BAD_REQUEST,
866 }
867}
868
869fn stream_error(error: anyhow::Error) -> ServerlessStreamError {
870 let extracted = rivet_error::RivetError::extract(&error);
871 ServerlessStreamError {
872 group: extracted.group().to_owned(),
873 code: extracted.code().to_owned(),
874 message: extracted.message().to_owned(),
875 }
876}
877
878pub fn normalize_endpoint_url(url: &str) -> Option<String> {
879 let parsed = Url::parse(url).ok()?;
880 let pathname = if parsed.path() == "/" {
881 "/".to_owned()
882 } else {
883 parsed.path().trim_end_matches('/').to_owned()
884 };
885 let mut hostname = parsed.host_str()?.to_owned();
886 if is_loopback_address(&hostname) {
887 hostname = "localhost".to_owned();
888 }
889 hostname = normalize_regional_hostname(&hostname);
890 let host = match parsed.port() {
891 Some(port) => format!("{hostname}:{port}"),
892 None => hostname,
893 };
894 Some(format!("{}://{}{}", parsed.scheme(), host, pathname))
895}
896
897fn normalized_endpoint_candidates(value: &str) -> Vec<String> {
898 value
899 .split(',')
900 .map(str::trim)
901 .filter(|candidate| !candidate.is_empty())
902 .map(|candidate| normalize_endpoint_url(candidate).unwrap_or_else(|| candidate.to_owned()))
903 .collect()
904}
905
906async fn wait_for_envoy_start(
909 handle: &EnvoyHandle,
910 start_deadline: Instant,
911) -> std::result::Result<(), String> {
912 let remaining = start_deadline.saturating_duration_since(Instant::now());
913 match timeout(remaining, handle.started()).await {
914 Ok(Ok(())) => Ok(()),
915 Ok(Err(error)) => Err(format!("{error:#}")),
916 Err(_) => Err(format!(
917 "no init message within {} ms",
918 ENVOY_START_TIMEOUT.as_millis()
919 )),
920 }
921}
922
923pub fn endpoints_match(a: &str, b: &str) -> bool {
924 let a_candidates = normalized_endpoint_candidates(a);
925 let b_candidates = normalized_endpoint_candidates(b);
926 a_candidates.iter().any(|a_candidate| {
927 b_candidates
928 .iter()
929 .any(|b_candidate| a_candidate == b_candidate)
930 })
931}
932
933fn normalize_regional_hostname(hostname: &str) -> String {
934 if !hostname.ends_with(".rivet.dev") || !hostname.starts_with("api-") {
935 return hostname.to_owned();
936 }
937 let without_prefix = &hostname[4..];
938 let Some(first_dot_index) = without_prefix.find('.') else {
939 return hostname.to_owned();
940 };
941 let domain = &without_prefix[first_dot_index + 1..];
942 format!("api.{domain}")
943}
944
945fn is_loopback_address(hostname: &str) -> bool {
946 matches!(hostname, "127.0.0.1" | "0.0.0.0" | "::1" | "[::1]")
947}
948
949