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 as start_envoy_client;
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::{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);
39
40#[derive(Clone)]
41pub struct CoreServerlessRuntime {
42 settings: Arc<ServerlessSettings>,
43 dispatcher: Arc<RegistryDispatcher>,
44 envoy: Arc<TokioMutex<Option<EnvoyHandle>>>,
45 #[cfg(feature = "native-runtime")]
46 development_processes: Arc<TokioMutex<Option<DevelopmentProcessManager>>>,
47 shutting_down: Arc<AtomicBool>,
48}
49
50#[derive(Clone, Debug)]
51struct ServerlessSettings {
52 version: u32,
53 configured_endpoint: String,
54 configured_namespace: String,
55 base_path: String,
56 package_version: String,
57 client_endpoint: Option<String>,
58 client_namespace: Option<String>,
59 client_token: Option<String>,
60 validate_endpoint: bool,
61 max_start_payload_bytes: usize,
62 cache_envoy: bool,
63}
64
65#[derive(Debug)]
66pub struct ServerlessRequest {
67 pub method: String,
68 pub url: String,
69 pub headers: HashMap<String, String>,
70 pub body: Vec<u8>,
71 pub cancel_token: CancellationToken,
72}
73
74#[derive(Debug)]
75pub struct ServerlessResponse {
76 pub status: u16,
77 pub headers: HashMap<String, String>,
78 pub body: mpsc::UnboundedReceiver<Result<Vec<u8>, ServerlessStreamError>>,
79}
80
81#[derive(Clone, Debug, Serialize)]
82pub struct ServerlessStreamError {
83 pub group: String,
84 pub code: String,
85 pub message: String,
86}
87
88#[derive(Debug)]
89struct StartHeaders {
90 endpoint: String,
91 token: Option<String>,
92 pool_name: String,
93 namespace: String,
94}
95
96#[derive(Debug, Serialize)]
97struct ServerlessErrorBody<'a> {
98 group: &'a str,
99 code: &'a str,
100 message: String,
101 metadata: serde_json::Value,
102}
103
104#[derive(rivet_error::RivetError, Serialize)]
105#[error("request", "invalid", "Invalid request.", "Invalid request: {reason}")]
106struct InvalidRequest {
107 reason: String,
108}
109
110#[derive(rivet_error::RivetError, Serialize)]
111#[error(
112 "config",
113 "endpoint_mismatch",
114 "Endpoint mismatch.",
115 "Endpoint mismatch: expected \"{expected}\", received \"{received}\""
116)]
117struct EndpointMismatch {
118 expected: String,
119 received: String,
120}
121
122#[derive(rivet_error::RivetError, Serialize)]
123#[error(
124 "config",
125 "namespace_mismatch",
126 "Namespace mismatch.",
127 "Namespace mismatch: expected \"{expected}\", received \"{received}\""
128)]
129struct NamespaceMismatch {
130 expected: String,
131 received: String,
132}
133
134#[derive(rivet_error::RivetError, Serialize)]
135#[error(
136 "message",
137 "incoming_too_long",
138 "Incoming message too long.",
139 "Incoming message too long. Exceeded limit of {limit} bytes."
140)]
141struct IncomingMessageTooLong {
142 limit: usize,
143}
144
145#[derive(rivet_error::RivetError, Serialize)]
146#[error(
147 "registry",
148 "shut_down",
149 "Registry is shut down.",
150 "Registry is shut down; no new requests can be accepted."
151)]
152struct RuntimeShutDown;
153
154impl CoreServerlessRuntime {
155 pub(crate) async fn new(
156 factories: HashMap<String, Arc<ActorFactory>>,
157 config: ServeConfig,
158 ) -> Result<Self> {
159 let manage_engine = should_manage_engine(&config.endpoint, config.engine_spawn)?;
160 #[cfg(feature = "native-runtime")]
161 let development_processes = if manage_engine {
162 Some(DevelopmentProcessManager::start(&config).await?)
163 } else {
164 None
165 };
166 #[cfg(not(feature = "native-runtime"))]
167 if manage_engine {
168 anyhow::bail!("engine process spawning requires the `native-runtime` feature");
169 }
170
171 let dispatcher = Arc::new(RegistryDispatcher::new(
172 factories,
173 config.handle_inspector_http_in_runtime,
174 ));
175 let base_path = normalize_base_path(config.serverless_base_path.as_deref());
176 crate::metrics::record_rivetkit_info(
177 config.serverless_package_version.clone(),
178 config.version,
179 "serverless",
180 config.pool_name.clone(),
181 );
182 #[cfg(not(target_arch = "wasm32"))]
183 crate::tokio_runtime_metrics::ensure_sampler_started();
184
185 Ok(Self {
186 settings: Arc::new(ServerlessSettings {
187 version: config.version,
188 configured_endpoint: config.endpoint,
189 configured_namespace: config.namespace,
190 base_path,
191 package_version: config.serverless_package_version,
192 client_endpoint: config.serverless_client_endpoint,
193 client_namespace: config.serverless_client_namespace,
194 client_token: config.serverless_client_token,
195 validate_endpoint: config.serverless_validate_endpoint,
196 max_start_payload_bytes: config.serverless_max_start_payload_bytes,
197 cache_envoy: config.serverless_cache_envoy,
198 }),
199 dispatcher,
200 envoy: Arc::new(TokioMutex::new(None)),
201 #[cfg(feature = "native-runtime")]
202 development_processes: Arc::new(TokioMutex::new(development_processes)),
203 shutting_down: Arc::new(AtomicBool::new(false)),
204 })
205 }
206
207 pub async fn shutdown(&self) {
214 self.shutting_down.store(true, Ordering::Release);
215 let handle = { self.envoy.lock().await.take() };
216 #[cfg(feature = "native-runtime")]
217 let development_processes = { self.development_processes.lock().await.take() };
218 let shutdown_envoy = async move {
219 if let Some(handle) = handle {
220 match timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.shutdown_and_wait(false)).await {
221 Ok(()) => {}
222 Err(_) => {
223 tracing::warn!(
224 "serverless runtime envoy drain exceeded timeout; forcing immediate stop"
225 );
226 handle.shutdown(true);
227 handle.wait_stopped().await;
228 }
229 }
230 }
231 };
232 #[cfg(feature = "native-runtime")]
233 let shutdown_development_processes = async move {
234 if let Some(development_processes) = development_processes {
235 development_processes.shutdown().await;
236 }
237 };
238 #[cfg(feature = "native-runtime")]
239 tokio::join!(shutdown_envoy, shutdown_development_processes);
240 #[cfg(not(feature = "native-runtime"))]
241 shutdown_envoy.await;
242 }
243
244 pub async fn wait_actors_drained(&self, timeout_dur: Duration) {
248 let handle = { self.envoy.lock().await.as_ref().cloned() };
249 let Some(handle) = handle else { return };
250 let _ = timeout(
251 timeout_dur,
252 CoreEnvoyHandle::new(handle).wait_actors_drained(),
253 )
254 .await;
255 }
256
257 pub async fn active_envoy_actor_count(&self) -> Option<usize> {
258 self.active_envoy_status()
259 .await
260 .map(|status| status.active_actor_count)
261 }
262
263 pub async fn active_envoy_status(&self) -> Option<CoreEnvoyStatus> {
264 self.envoy
265 .lock()
266 .await
267 .as_ref()
268 .map(|handle| CoreEnvoyHandle::new(handle.clone()).status())
269 }
270
271 pub async fn active_envoy_actor_stop_threshold_ms(&self) -> Option<i64> {
272 let handle = self.envoy.lock().await.as_ref().cloned()?;
273 CoreEnvoyHandle::new(handle).actor_stop_threshold_ms().await
274 }
275
276 pub fn max_request_body_bytes(&self) -> usize {
278 self.settings.max_start_payload_bytes
279 }
280
281 pub fn handles_listener_request(&self, url: &str) -> bool {
285 handles_listener_request(&self.settings.base_path, url)
286 }
287
288 pub fn incoming_too_long_response(&self) -> ServerlessResponse {
290 let error = IncomingMessageTooLong {
291 limit: self.settings.max_start_payload_bytes,
292 }
293 .build();
294 error_response(error)
295 }
296
297 pub fn invalid_request_response(&self, reason: impl Into<String>) -> ServerlessResponse {
299 let error = InvalidRequest {
300 reason: reason.into(),
301 }
302 .build();
303 error_response(error)
304 }
305
306 pub async fn handle_request(&self, req: ServerlessRequest) -> ServerlessResponse {
307 let cors = cors_headers(&req);
308 match self.handle_request_inner(req).await {
309 Ok(mut response) => {
310 apply_cors(&mut response.headers, cors);
311 response
312 }
313 Err(error) => {
314 let mut response = error_response(error);
315 apply_cors(&mut response.headers, cors);
316 response
317 }
318 }
319 }
320
321 async fn handle_request_inner(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
322 let path = route_path(&self.settings.base_path, &req.url)?;
323 match (req.method.as_str(), path.as_str()) {
324 ("GET", "") | ("GET", "/") => Ok(text_response(
325 StatusCode::OK,
326 "text/plain; charset=utf-8",
327 "This is a RivetKit server.\n\nLearn more at https://rivet.dev",
328 )),
329 ("GET", "/health") => {
330 let runtime_healthy = {
335 let guard = self.envoy.lock().await;
336 guard
337 .as_ref()
338 .map(|handle| handle.is_ping_healthy())
339 .unwrap_or(true)
340 };
341 if runtime_healthy {
342 Ok(json_response(
343 StatusCode::OK,
344 json!({
345 "status": "ok",
346 "runtime": "rivetkit",
347 "version": self.settings.package_version,
348 }),
349 ))
350 } else {
351 Ok(json_response(
352 StatusCode::SERVICE_UNAVAILABLE,
353 json!({
354 "status": "engine_ping_stale",
355 "runtime": "rivetkit",
356 "version": self.settings.package_version,
357 }),
358 ))
359 }
360 }
361 ("GET", "/metadata") => Ok(self.metadata_response()),
362 ("GET", "/start") | ("POST", "/start") => self.start_response(req).await,
363 ("OPTIONS", _) => Ok(bytes_response(
364 StatusCode::NO_CONTENT,
365 HashMap::new(),
366 Vec::new(),
367 )),
368 _ => Ok(text_response(
369 StatusCode::NOT_FOUND,
370 "text/plain; charset=utf-8",
371 "Not Found (RivetKit)",
372 )),
373 }
374 }
375
376 async fn start_response(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
377 let headers = parse_start_headers(&req.headers)?;
378 self.validate_start_headers(&headers)?;
379 crate::metrics::record_rivetkit_info(
380 self.settings.package_version.clone(),
381 self.settings.version,
382 "serverless",
383 headers.pool_name.clone(),
384 );
385 #[cfg(not(target_arch = "wasm32"))]
386 crate::tokio_runtime_metrics::ensure_sampler_started();
387 if req.body.len() > self.settings.max_start_payload_bytes {
388 return Err(IncomingMessageTooLong {
389 limit: self.settings.max_start_payload_bytes,
390 }
391 .build());
392 }
393
394 let handle = self.ensure_envoy(&headers).await?;
395 let payload = req.body;
396 let actor_start = handle.decode_serverless_actor_start(&payload)?;
397 let cancel_token = req.cancel_token;
398 let cache_envoy = self.settings.cache_envoy;
399 let (tx, rx) = mpsc::unbounded_channel();
400 let _ = tx.send(Ok(SSE_PING_FRAME.to_vec()));
401
402 RuntimeSpawner::spawn(async move {
403 let shutdown_handle = handle.clone();
404 let result = tokio::select! {
405 _ = cancel_token.cancelled() => {
406 if !cache_envoy {
407 shutdown_handle.shutdown_and_wait(false).await;
408 }
409 return;
410 }
411 result = handle.start_serverless_actor(&payload) => result,
412 };
413 if let Err(error) = result {
414 let error = stream_error(error);
415 let _ = tx.send(Err(error));
416 if !cache_envoy {
417 handle.shutdown_and_wait(false).await;
418 }
419 return;
420 }
421
422 loop {
423 tokio::select! {
424 _ = cancel_token.cancelled() => {
425 break;
426 }
427 _ = handle.wait_actor_registered_then_stopped(&actor_start.actor_id, actor_start.generation) => {
428 let _ = tx.send(Ok(SSE_STOPPING_FRAME.to_vec()));
429 break;
430 }
431 _ = sleep(SSE_PING_INTERVAL) => {
432 if tx.send(Ok(SSE_PING_FRAME.to_vec())).is_err() {
433 break;
434 }
435 }
436 }
437 }
438
439 if !cache_envoy {
440 handle.shutdown_and_wait(false).await;
441 }
442 });
443
444 Ok(ServerlessResponse {
445 status: StatusCode::OK.as_u16(),
446 headers: HashMap::from([
447 ("content-type".to_owned(), "text/event-stream".to_owned()),
448 ("cache-control".to_owned(), "no-cache".to_owned()),
449 ("connection".to_owned(), "keep-alive".to_owned()),
450 ]),
451 body: rx,
452 })
453 }
454
455 fn metadata_response(&self) -> ServerlessResponse {
456 let actor_names = self
457 .dispatcher
458 .build_actor_metadata_map()
459 .into_iter()
460 .map(|(name, metadata)| {
461 (
462 name,
463 ActorName {
464 metadata: Some(metadata),
465 },
466 )
467 })
468 .collect::<HashMap<_, _>>();
469
470 let payload = ServerlessMetadataPayload {
471 runtime: "rivetkit".to_owned(),
472 version: self.settings.package_version.clone(),
473 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
474 actor_names,
475 envoy: Some(ServerlessMetadataEnvoy {
476 kind: Some(ServerlessMetadataEnvoyKind::Serverless {}),
477 version: Some(self.settings.version),
478 }),
479 runner: None,
480 client_endpoint: self.settings.client_endpoint.clone(),
481 client_namespace: self.settings.client_namespace.clone(),
482 client_token: self.settings.client_token.clone(),
483 };
484
485 let response = serde_json::to_value(payload).unwrap_or_else(|_| json!({}));
486
487 json_response(StatusCode::OK, response)
488 }
489
490 fn validate_start_headers(&self, headers: &StartHeaders) -> Result<()> {
491 if self.settings.validate_endpoint {
492 if !endpoints_match(&headers.endpoint, &self.settings.configured_endpoint) {
493 tracing::warn!(
494 configured_endpoint = %self.settings.configured_endpoint,
495 received_endpoint = %headers.endpoint,
496 "serverless start rejected: endpoint mismatch",
497 );
498 return Err(EndpointMismatch {
499 expected: self.settings.configured_endpoint.clone(),
500 received: headers.endpoint.clone(),
501 }
502 .build());
503 }
504
505 if headers.namespace != self.settings.configured_namespace {
506 tracing::warn!(
507 configured_namespace = %self.settings.configured_namespace,
508 received_namespace = %headers.namespace,
509 "serverless start rejected: namespace mismatch",
510 );
511 return Err(NamespaceMismatch {
512 expected: self.settings.configured_namespace.clone(),
513 received: headers.namespace.clone(),
514 }
515 .build());
516 }
517 }
518
519 Ok(())
520 }
521
522 async fn ensure_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
523 if self.shutting_down.load(Ordering::Acquire) {
524 return Err(RuntimeShutDown.build());
525 }
526 if !self.settings.cache_envoy {
527 return self.start_envoy(headers).await;
528 }
529 let mut guard = self.envoy.lock().await;
530 if let Some(handle) = guard.as_ref() {
531 if !endpoints_match(handle.endpoint(), &headers.endpoint)
534 || handle.namespace() != headers.namespace
535 || handle.pool_name() != headers.pool_name
536 {
537 anyhow::bail!("serverless start headers do not match active envoy");
538 }
539 return Ok(handle.clone());
540 }
541
542 let handle = self.start_envoy(headers).await?;
543 if self.shutting_down.load(Ordering::Acquire) {
547 drop(guard);
548 match timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.shutdown_and_wait(false)).await {
549 Ok(()) => {}
550 Err(_) => {
551 handle.shutdown(true);
552 handle.wait_stopped().await;
553 }
554 }
555 return Err(RuntimeShutDown.build());
556 }
557 *guard = Some(handle.clone());
558 Ok(handle)
559 }
560
561 async fn start_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
562 let callbacks = Arc::new(RegistryCallbacks {
563 dispatcher: self.dispatcher.clone(),
564 });
565 let prepopulate_actor_names = self
566 .dispatcher
567 .build_actor_metadata_map()
568 .into_iter()
569 .map(|(name, metadata)| (name, EnvoyActorName { metadata }))
570 .collect();
571 Ok(start_envoy_client(EnvoyConfig {
576 version: self.settings.version,
577 endpoint: headers.endpoint.clone(),
578 token: headers.token.clone(),
579 namespace: headers.namespace.clone(),
580 pool_name: headers.pool_name.clone(),
581 prepopulate_actor_names,
582 metadata: Some(json!({
583 "rivetkit": { "version": self.settings.package_version },
584 })),
585 not_global: true,
586 debug_latency_ms: None,
587 callbacks,
588 })
589 .await)
590 }
591}
592
593fn route_path(base_path: &str, url: &str) -> Result<String> {
594 let parsed = Url::parse(url).with_context(|| format!("parse request URL `{url}`"))?;
595 let path = parsed.path();
596 if path == base_path {
597 return Ok(String::new());
598 }
599 let prefix = format!("{base_path}/");
600 if let Some(rest) = path.strip_prefix(&prefix) {
601 return Ok(format!("/{rest}"));
602 }
603 Ok(path.to_owned())
604}
605
606fn handles_listener_request(base_path: &str, url: &str) -> bool {
607 let Ok(parsed) = Url::parse(url) else {
608 return true;
611 };
612 let request_path = parsed.path();
613 if request_path != base_path && !request_path.starts_with(&format!("{base_path}/")) {
614 return false;
615 }
616 let path = route_path(base_path, url).expect("URL was parsed and the same base path is valid");
617 matches!(path.as_str(), "" | "/" | "/health" | "/metadata" | "/start")
618}
619
620fn parse_start_headers(headers: &HashMap<String, String>) -> Result<StartHeaders> {
621 let pool_name = match optional_header(headers, "x-rivet-pool-name") {
622 Some(pool_name) => pool_name,
623 None => optional_header(headers, "x-rivet-runner-name").ok_or_else(|| {
624 InvalidRequest {
625 reason: "x-rivet-pool-name header is required".to_string(),
626 }
627 .build()
628 })?,
629 };
630
631 Ok(StartHeaders {
632 endpoint: required_header(headers, "x-rivet-endpoint")?,
633 token: optional_header(headers, "x-rivet-token"),
634 pool_name,
635 namespace: required_header(headers, "x-rivet-namespace-name")?,
636 })
637}
638
639fn required_header(headers: &HashMap<String, String>, name: &str) -> Result<String> {
640 headers
641 .get(name)
642 .filter(|value| !value.is_empty())
643 .cloned()
644 .ok_or_else(|| {
645 InvalidRequest {
646 reason: format!("{name} header is required"),
647 }
648 .build()
649 })
650}
651
652fn optional_header(headers: &HashMap<String, String>, name: &str) -> Option<String> {
653 headers.get(name).filter(|value| !value.is_empty()).cloned()
654}
655
656fn cors_headers(req: &ServerlessRequest) -> HashMap<String, String> {
657 let origin = req
658 .headers
659 .get("origin")
660 .cloned()
661 .unwrap_or_else(|| "*".to_owned());
662 let mut headers = HashMap::from([
663 ("access-control-allow-origin".to_owned(), origin.clone()),
664 (
665 "access-control-allow-credentials".to_owned(),
666 "true".to_owned(),
667 ),
668 ("access-control-expose-headers".to_owned(), "*".to_owned()),
669 ]);
670 if origin != "*" {
671 headers.insert("vary".to_owned(), "Origin".to_owned());
672 }
673
674 if req.method == "OPTIONS" {
675 headers.insert(
676 "access-control-allow-methods".to_owned(),
677 "GET, POST, PUT, DELETE, OPTIONS, PATCH".to_owned(),
678 );
679 headers.insert(
680 "access-control-allow-headers".to_owned(),
681 req.headers
682 .get("access-control-request-headers")
683 .cloned()
684 .unwrap_or_else(|| "*".to_owned()),
685 );
686 headers.insert("access-control-max-age".to_owned(), "86400".to_owned());
687 }
688
689 headers
690}
691
692fn apply_cors(headers: &mut HashMap<String, String>, cors: HashMap<String, String>) {
693 headers.extend(cors);
694}
695
696fn normalize_base_path(base_path: Option<&str>) -> String {
697 let base_path = base_path
698 .filter(|base_path| !base_path.is_empty())
699 .unwrap_or(DEFAULT_BASE_PATH);
700 let prefixed = if base_path.starts_with('/') {
701 base_path.to_owned()
702 } else {
703 format!("/{base_path}")
704 };
705 let trimmed = prefixed.trim_end_matches('/');
706 if trimmed.is_empty() {
707 "/".to_owned()
708 } else {
709 trimmed.to_owned()
710 }
711}
712
713fn text_response(status: StatusCode, content_type: &str, body: &str) -> ServerlessResponse {
714 bytes_response(
715 status,
716 HashMap::from([("content-type".to_owned(), content_type.to_owned())]),
717 body.as_bytes().to_vec(),
718 )
719}
720
721fn json_response(status: StatusCode, body: serde_json::Value) -> ServerlessResponse {
722 bytes_response(
723 status,
724 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
725 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
726 )
727}
728
729fn bytes_response(
730 status: StatusCode,
731 headers: HashMap<String, String>,
732 body: Vec<u8>,
733) -> ServerlessResponse {
734 let (tx, rx) = mpsc::unbounded_channel();
735 let _ = tx.send(Ok(body));
736 ServerlessResponse {
737 status: status.as_u16(),
738 headers,
739 body: rx,
740 }
741}
742
743fn error_response(error: anyhow::Error) -> ServerlessResponse {
744 let extracted = rivet_error::RivetError::extract(&error);
745 let status = serverless_error_status(extracted.group(), extracted.code());
746 let body = ServerlessErrorBody {
747 group: extracted.group(),
748 code: extracted.code(),
749 message: extracted.message().to_owned(),
750 metadata: extracted.metadata().unwrap_or(serde_json::Value::Null),
751 };
752 bytes_response(
753 status,
754 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
755 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
756 )
757}
758
759fn serverless_error_status(group: &str, code: &str) -> StatusCode {
760 match (group, code) {
761 ("auth", "forbidden") => StatusCode::FORBIDDEN,
762 ("message", "incoming_too_long") => StatusCode::PAYLOAD_TOO_LARGE,
763 _ => StatusCode::BAD_REQUEST,
764 }
765}
766
767fn stream_error(error: anyhow::Error) -> ServerlessStreamError {
768 let extracted = rivet_error::RivetError::extract(&error);
769 ServerlessStreamError {
770 group: extracted.group().to_owned(),
771 code: extracted.code().to_owned(),
772 message: extracted.message().to_owned(),
773 }
774}
775
776pub fn normalize_endpoint_url(url: &str) -> Option<String> {
777 let parsed = Url::parse(url).ok()?;
778 let pathname = if parsed.path() == "/" {
779 "/".to_owned()
780 } else {
781 parsed.path().trim_end_matches('/').to_owned()
782 };
783 let mut hostname = parsed.host_str()?.to_owned();
784 if is_loopback_address(&hostname) {
785 hostname = "localhost".to_owned();
786 }
787 hostname = normalize_regional_hostname(&hostname);
788 let host = match parsed.port() {
789 Some(port) => format!("{hostname}:{port}"),
790 None => hostname,
791 };
792 Some(format!("{}://{}{}", parsed.scheme(), host, pathname))
793}
794
795fn normalized_endpoint_candidates(value: &str) -> Vec<String> {
796 value
797 .split(',')
798 .map(str::trim)
799 .filter(|candidate| !candidate.is_empty())
800 .map(|candidate| normalize_endpoint_url(candidate).unwrap_or_else(|| candidate.to_owned()))
801 .collect()
802}
803
804pub fn endpoints_match(a: &str, b: &str) -> bool {
805 let a_candidates = normalized_endpoint_candidates(a);
806 let b_candidates = normalized_endpoint_candidates(b);
807 a_candidates.iter().any(|a_candidate| {
808 b_candidates
809 .iter()
810 .any(|b_candidate| a_candidate == b_candidate)
811 })
812}
813
814fn normalize_regional_hostname(hostname: &str) -> String {
815 if !hostname.ends_with(".rivet.dev") || !hostname.starts_with("api-") {
816 return hostname.to_owned();
817 }
818 let without_prefix = &hostname[4..];
819 let Some(first_dot_index) = without_prefix.find('.') else {
820 return hostname.to_owned();
821 };
822 let domain = &without_prefix[first_dot_index + 1..];
823 format!("api.{domain}")
824}
825
826fn is_loopback_address(hostname: &str) -> bool {
827 matches!(hostname, "127.0.0.1" | "0.0.0.0" | "::1" | "[::1]")
828}
829
830#[cfg(test)]
832#[path = "../tests/serverless.rs"]
833mod tests;