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