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 runtime_healthy = handle
373 .map(|handle| !handle.has_started() || handle.is_ping_healthy())
374 .unwrap_or(true);
375 if runtime_healthy {
376 Ok(json_response(
377 StatusCode::OK,
378 json!({
379 "status": "ok",
380 "runtime": "rivetkit",
381 "version": self.settings.package_version,
382 }),
383 ))
384 } else {
385 Ok(json_response(
386 StatusCode::SERVICE_UNAVAILABLE,
387 json!({
388 "status": "engine_ping_stale",
389 "runtime": "rivetkit",
390 "version": self.settings.package_version,
391 }),
392 ))
393 }
394 }
395 ("GET", "/metadata") => Ok(self.metadata_response()),
396 ("GET", "/start") | ("POST", "/start") => self.start_response(req).await,
397 ("OPTIONS", _) => Ok(bytes_response(
398 StatusCode::NO_CONTENT,
399 HashMap::new(),
400 Vec::new(),
401 )),
402 _ => Ok(text_response(
403 StatusCode::NOT_FOUND,
404 "text/plain; charset=utf-8",
405 "Not Found (RivetKit)",
406 )),
407 }
408 }
409
410 async fn start_response(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
411 let headers = parse_start_headers(&req.headers)?;
412 self.validate_start_headers(&headers)?;
413 crate::metrics::record_rivetkit_info(
414 self.settings.package_version.clone(),
415 self.settings.version,
416 "serverless",
417 headers.pool_name.clone(),
418 );
419 #[cfg(not(target_arch = "wasm32"))]
420 crate::tokio_runtime_metrics::ensure_sampler_started();
421 if req.body.len() > self.settings.max_start_payload_bytes {
422 return Err(IncomingMessageTooLong {
423 limit: self.settings.max_start_payload_bytes,
424 }
425 .build());
426 }
427
428 let handle = self.ensure_envoy(&headers).await?;
429 let payload = req.body;
430 let actor_start = handle.decode_serverless_actor_start(&payload)?;
431 let cancel_token = req.cancel_token;
432 let cache_envoy = self.settings.cache_envoy;
433 let (tx, rx) = mpsc::unbounded_channel();
434 let _ = tx.send(Ok(SSE_PING_FRAME.to_vec()));
435
436 RuntimeSpawner::spawn(async move {
437 let shutdown_handle = handle.clone();
438 let result = tokio::select! {
439 _ = cancel_token.cancelled() => {
440 if !cache_envoy {
441 shutdown_handle.shutdown_and_wait(false).await;
442 }
443 return;
444 }
445 result = handle.start_serverless_actor(&payload) => result,
446 };
447 if let Err(error) = result {
448 let error = stream_error(error);
449 let _ = tx.send(Err(error));
450 if !cache_envoy {
451 handle.shutdown_and_wait(false).await;
452 }
453 return;
454 }
455
456 loop {
457 tokio::select! {
458 _ = cancel_token.cancelled() => {
459 break;
460 }
461 _ = handle.wait_actor_registered_then_stopped(&actor_start.actor_id, actor_start.generation) => {
462 let _ = tx.send(Ok(SSE_STOPPING_FRAME.to_vec()));
463 break;
464 }
465 _ = sleep(SSE_PING_INTERVAL) => {
466 if tx.send(Ok(SSE_PING_FRAME.to_vec())).is_err() {
467 break;
468 }
469 }
470 }
471 }
472
473 if !cache_envoy {
474 handle.shutdown_and_wait(false).await;
475 }
476 });
477
478 Ok(ServerlessResponse {
479 status: StatusCode::OK.as_u16(),
480 headers: HashMap::from([
481 ("content-type".to_owned(), "text/event-stream".to_owned()),
482 ("cache-control".to_owned(), "no-cache".to_owned()),
483 ("connection".to_owned(), "keep-alive".to_owned()),
484 ]),
485 body: rx,
486 })
487 }
488
489 fn metadata_response(&self) -> ServerlessResponse {
490 let actor_names = self
491 .dispatcher
492 .build_actor_metadata_map()
493 .into_iter()
494 .map(|(name, metadata)| {
495 (
496 name,
497 ActorName {
498 metadata: Some(metadata),
499 },
500 )
501 })
502 .collect::<HashMap<_, _>>();
503
504 let payload = ServerlessMetadataPayload {
505 runtime: "rivetkit".to_owned(),
506 version: self.settings.package_version.clone(),
507 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
508 actor_names,
509 envoy: Some(ServerlessMetadataEnvoy {
510 kind: Some(ServerlessMetadataEnvoyKind::Serverless {}),
511 version: Some(self.settings.version),
512 }),
513 runner: None,
514 client_endpoint: self.settings.client_endpoint.clone(),
515 client_namespace: self.settings.client_namespace.clone(),
516 client_token: self.settings.client_token.clone(),
517 };
518
519 let response = serde_json::to_value(payload).unwrap_or_else(|_| json!({}));
520
521 json_response(StatusCode::OK, response)
522 }
523
524 fn validate_start_headers(&self, headers: &StartHeaders) -> Result<()> {
525 if self.settings.validate_endpoint {
526 if !endpoints_match(&headers.endpoint, &self.settings.configured_endpoint) {
527 tracing::warn!(
528 configured_endpoint = %self.settings.configured_endpoint,
529 received_endpoint = %headers.endpoint,
530 "serverless start rejected: endpoint mismatch",
531 );
532 return Err(EndpointMismatch {
533 expected: self.settings.configured_endpoint.clone(),
534 received: headers.endpoint.clone(),
535 }
536 .build());
537 }
538
539 if headers.namespace != self.settings.configured_namespace {
540 tracing::warn!(
541 configured_namespace = %self.settings.configured_namespace,
542 received_namespace = %headers.namespace,
543 "serverless start rejected: namespace mismatch",
544 );
545 return Err(NamespaceMismatch {
546 expected: self.settings.configured_namespace.clone(),
547 received: headers.namespace.clone(),
548 }
549 .build());
550 }
551 }
552
553 Ok(())
554 }
555
556 async fn ensure_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
562 if self.shutting_down.load(Ordering::Acquire) {
563 return Err(RuntimeShutDown.build());
564 }
565 if !self.settings.cache_envoy {
566 let handle = start_envoy_sync(self.envoy_config(headers));
567 let start_deadline = Instant::now() + ENVOY_START_TIMEOUT;
568 if let Err(reason) = wait_for_envoy_start(&handle, start_deadline).await {
569 return Err(self.stop_failed_envoy(&handle, reason).await);
570 }
571 return Ok(handle);
572 }
573
574 let cached = {
575 let mut guard = self.envoy.lock().await;
576 if self.shutting_down.load(Ordering::Acquire) {
580 return Err(RuntimeShutDown.build());
581 }
582 match guard.as_ref() {
583 Some(cached) => {
584 let handle = &cached.handle;
588 if !endpoints_match(handle.endpoint(), &headers.endpoint)
589 || handle.namespace() != headers.namespace
590 || handle.pool_name() != headers.pool_name
591 {
592 anyhow::bail!("serverless start headers do not match active envoy");
593 }
594 cached.clone()
595 }
596 None => {
597 let cached = CachedEnvoy {
598 handle: start_envoy_sync(self.envoy_config(headers)),
599 start_deadline: Instant::now() + ENVOY_START_TIMEOUT,
600 };
601 *guard = Some(cached.clone());
602 cached
603 }
604 }
605 };
606
607 if let Err(reason) = wait_for_envoy_start(&cached.handle, cached.start_deadline).await {
608 {
611 let mut guard = self.envoy.lock().await;
612 let is_cached = guard.as_ref().is_some_and(|current| {
613 current.handle.get_envoy_key() == cached.handle.get_envoy_key()
614 });
615 if is_cached {
616 *guard = None;
617 }
618 }
619 return Err(self.stop_failed_envoy(&cached.handle, reason).await);
620 }
621 Ok(cached.handle)
622 }
623
624 async fn stop_failed_envoy(&self, handle: &EnvoyHandle, reason: String) -> anyhow::Error {
627 handle.shutdown(true);
628 if timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.wait_stopped())
629 .await
630 .is_err()
631 {
632 tracing::warn!(
633 "serverless envoy did not stop after a failed start; the engine may be unreachable"
634 );
635 }
636
637 if self.shutting_down.load(Ordering::Acquire) {
638 return RuntimeShutDown.build();
639 }
640 tracing::warn!(%reason, "serverless envoy failed to start; check engine reachability");
641 EnvoyStartFailed { reason }.build()
642 }
643
644 fn envoy_config(&self, headers: &StartHeaders) -> EnvoyConfig {
645 let callbacks = Arc::new(RegistryCallbacks {
646 dispatcher: self.dispatcher.clone(),
647 });
648 let prepopulate_actor_names = self
649 .dispatcher
650 .build_actor_metadata_map()
651 .into_iter()
652 .map(|(name, metadata)| (name, EnvoyActorName { metadata }))
653 .collect();
654 EnvoyConfig {
659 version: self.settings.version,
660 endpoint: headers.endpoint.clone(),
661 token: headers.token.clone(),
662 namespace: headers.namespace.clone(),
663 pool_name: headers.pool_name.clone(),
664 prepopulate_actor_names,
665 metadata: Some(json!({
666 "rivetkit": { "version": self.settings.package_version },
667 })),
668 not_global: true,
669 debug_latency_ms: None,
670 max_queued_requests_per_actor:
671 rivet_envoy_client::config::DEFAULT_MAX_QUEUED_REQUESTS_PER_ACTOR,
672 stopped_generation_floor_ttl:
673 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_TTL,
674 stopped_generation_floor_capacity:
675 rivet_envoy_client::config::DEFAULT_STOPPED_GENERATION_FLOOR_CAPACITY,
676 callbacks,
677 connector: None,
678 }
679 }
680}
681
682fn route_path(base_path: &str, url: &str) -> Result<String> {
683 let parsed = Url::parse(url).with_context(|| format!("parse request URL `{url}`"))?;
684 let path = parsed.path();
685 if path == base_path {
686 return Ok(String::new());
687 }
688 let prefix = format!("{base_path}/");
689 if let Some(rest) = path.strip_prefix(&prefix) {
690 return Ok(format!("/{rest}"));
691 }
692 Ok(path.to_owned())
693}
694
695fn handles_listener_request(base_path: &str, url: &str) -> bool {
696 let Ok(parsed) = Url::parse(url) else {
697 return true;
700 };
701 let request_path = parsed.path();
702 if request_path != base_path && !request_path.starts_with(&format!("{base_path}/")) {
703 return false;
704 }
705 let path = route_path(base_path, url).expect("URL was parsed and the same base path is valid");
706 matches!(path.as_str(), "" | "/" | "/health" | "/metadata" | "/start")
707}
708
709fn parse_start_headers(headers: &HashMap<String, String>) -> Result<StartHeaders> {
710 let pool_name = match optional_header(headers, "x-rivet-pool-name") {
711 Some(pool_name) => pool_name,
712 None => optional_header(headers, "x-rivet-runner-name").ok_or_else(|| {
713 InvalidRequest {
714 reason: "x-rivet-pool-name header is required".to_string(),
715 }
716 .build()
717 })?,
718 };
719
720 Ok(StartHeaders {
721 endpoint: required_header(headers, "x-rivet-endpoint")?,
722 token: optional_header(headers, "x-rivet-token"),
723 pool_name,
724 namespace: required_header(headers, "x-rivet-namespace-name")?,
725 })
726}
727
728fn required_header(headers: &HashMap<String, String>, name: &str) -> Result<String> {
729 headers
730 .get(name)
731 .filter(|value| !value.is_empty())
732 .cloned()
733 .ok_or_else(|| {
734 InvalidRequest {
735 reason: format!("{name} header is required"),
736 }
737 .build()
738 })
739}
740
741fn optional_header(headers: &HashMap<String, String>, name: &str) -> Option<String> {
742 headers.get(name).filter(|value| !value.is_empty()).cloned()
743}
744
745fn cors_headers(req: &ServerlessRequest) -> HashMap<String, String> {
746 let origin = req
747 .headers
748 .get("origin")
749 .cloned()
750 .unwrap_or_else(|| "*".to_owned());
751 let mut headers = HashMap::from([
752 ("access-control-allow-origin".to_owned(), origin.clone()),
753 (
754 "access-control-allow-credentials".to_owned(),
755 "true".to_owned(),
756 ),
757 ("access-control-expose-headers".to_owned(), "*".to_owned()),
758 ]);
759 if origin != "*" {
760 headers.insert("vary".to_owned(), "Origin".to_owned());
761 }
762
763 if req.method == "OPTIONS" {
764 headers.insert(
765 "access-control-allow-methods".to_owned(),
766 "GET, POST, PUT, DELETE, OPTIONS, PATCH".to_owned(),
767 );
768 headers.insert(
769 "access-control-allow-headers".to_owned(),
770 req.headers
771 .get("access-control-request-headers")
772 .cloned()
773 .unwrap_or_else(|| "*".to_owned()),
774 );
775 headers.insert("access-control-max-age".to_owned(), "86400".to_owned());
776 }
777
778 headers
779}
780
781fn apply_cors(headers: &mut HashMap<String, String>, cors: HashMap<String, String>) {
782 headers.extend(cors);
783}
784
785fn normalize_base_path(base_path: Option<&str>) -> String {
786 let base_path = base_path
787 .filter(|base_path| !base_path.is_empty())
788 .unwrap_or(DEFAULT_BASE_PATH);
789 let prefixed = if base_path.starts_with('/') {
790 base_path.to_owned()
791 } else {
792 format!("/{base_path}")
793 };
794 let trimmed = prefixed.trim_end_matches('/');
795 if trimmed.is_empty() {
796 "/".to_owned()
797 } else {
798 trimmed.to_owned()
799 }
800}
801
802fn text_response(status: StatusCode, content_type: &str, body: &str) -> ServerlessResponse {
803 bytes_response(
804 status,
805 HashMap::from([("content-type".to_owned(), content_type.to_owned())]),
806 body.as_bytes().to_vec(),
807 )
808}
809
810fn json_response(status: StatusCode, body: serde_json::Value) -> ServerlessResponse {
811 bytes_response(
812 status,
813 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
814 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
815 )
816}
817
818fn bytes_response(
819 status: StatusCode,
820 headers: HashMap<String, String>,
821 body: Vec<u8>,
822) -> ServerlessResponse {
823 let (tx, rx) = mpsc::unbounded_channel();
824 let _ = tx.send(Ok(body));
825 ServerlessResponse {
826 status: status.as_u16(),
827 headers,
828 body: rx,
829 }
830}
831
832fn error_response(error: anyhow::Error) -> ServerlessResponse {
833 let extracted = rivet_error::RivetError::extract(&error);
834 let status = serverless_error_status(extracted.group(), extracted.code());
835 let body = ServerlessErrorBody {
836 group: extracted.group(),
837 code: extracted.code(),
838 message: extracted.message().to_owned(),
839 metadata: extracted.metadata().unwrap_or(serde_json::Value::Null),
840 };
841 bytes_response(
842 status,
843 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
844 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
845 )
846}
847
848fn serverless_error_status(group: &str, code: &str) -> StatusCode {
849 match (group, code) {
850 ("auth", "forbidden") => StatusCode::FORBIDDEN,
851 ("message", "incoming_too_long") => StatusCode::PAYLOAD_TOO_LARGE,
852 ("registry", "envoy_start_failed") => StatusCode::SERVICE_UNAVAILABLE,
853 _ => StatusCode::BAD_REQUEST,
854 }
855}
856
857fn stream_error(error: anyhow::Error) -> ServerlessStreamError {
858 let extracted = rivet_error::RivetError::extract(&error);
859 ServerlessStreamError {
860 group: extracted.group().to_owned(),
861 code: extracted.code().to_owned(),
862 message: extracted.message().to_owned(),
863 }
864}
865
866pub fn normalize_endpoint_url(url: &str) -> Option<String> {
867 let parsed = Url::parse(url).ok()?;
868 let pathname = if parsed.path() == "/" {
869 "/".to_owned()
870 } else {
871 parsed.path().trim_end_matches('/').to_owned()
872 };
873 let mut hostname = parsed.host_str()?.to_owned();
874 if is_loopback_address(&hostname) {
875 hostname = "localhost".to_owned();
876 }
877 hostname = normalize_regional_hostname(&hostname);
878 let host = match parsed.port() {
879 Some(port) => format!("{hostname}:{port}"),
880 None => hostname,
881 };
882 Some(format!("{}://{}{}", parsed.scheme(), host, pathname))
883}
884
885fn normalized_endpoint_candidates(value: &str) -> Vec<String> {
886 value
887 .split(',')
888 .map(str::trim)
889 .filter(|candidate| !candidate.is_empty())
890 .map(|candidate| normalize_endpoint_url(candidate).unwrap_or_else(|| candidate.to_owned()))
891 .collect()
892}
893
894async fn wait_for_envoy_start(
897 handle: &EnvoyHandle,
898 start_deadline: Instant,
899) -> std::result::Result<(), String> {
900 let remaining = start_deadline.saturating_duration_since(Instant::now());
901 match timeout(remaining, handle.started()).await {
902 Ok(Ok(())) => Ok(()),
903 Ok(Err(error)) => Err(format!("{error:#}")),
904 Err(_) => Err(format!(
905 "no init message within {} ms",
906 ENVOY_START_TIMEOUT.as_millis()
907 )),
908 }
909}
910
911pub fn endpoints_match(a: &str, b: &str) -> bool {
912 let a_candidates = normalized_endpoint_candidates(a);
913 let b_candidates = normalized_endpoint_candidates(b);
914 a_candidates.iter().any(|a_candidate| {
915 b_candidates
916 .iter()
917 .any(|b_candidate| a_candidate == b_candidate)
918 })
919}
920
921fn normalize_regional_hostname(hostname: &str) -> String {
922 if !hostname.ends_with(".rivet.dev") || !hostname.starts_with("api-") {
923 return hostname.to_owned();
924 }
925 let without_prefix = &hostname[4..];
926 let Some(first_dot_index) = without_prefix.find('.') else {
927 return hostname.to_owned();
928 };
929 let domain = &without_prefix[first_dot_index + 1..];
930 format!("api.{domain}")
931}
932
933fn is_loopback_address(hostname: &str) -> bool {
934 matches!(hostname, "127.0.0.1" | "0.0.0.0" | "::1" | "[::1]")
935}
936
937