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_endpoint::record_rivetkit_info(
177 config.serverless_package_version.clone(),
178 config.version,
179 "serverless",
180 config.pool_name.clone(),
181 );
182
183 Ok(Self {
184 settings: Arc::new(ServerlessSettings {
185 version: config.version,
186 configured_endpoint: config.endpoint,
187 configured_namespace: config.namespace,
188 base_path,
189 package_version: config.serverless_package_version,
190 client_endpoint: config.serverless_client_endpoint,
191 client_namespace: config.serverless_client_namespace,
192 client_token: config.serverless_client_token,
193 validate_endpoint: config.serverless_validate_endpoint,
194 max_start_payload_bytes: config.serverless_max_start_payload_bytes,
195 cache_envoy: config.serverless_cache_envoy,
196 }),
197 dispatcher,
198 envoy: Arc::new(TokioMutex::new(None)),
199 #[cfg(feature = "native-runtime")]
200 development_processes: Arc::new(TokioMutex::new(development_processes)),
201 shutting_down: Arc::new(AtomicBool::new(false)),
202 })
203 }
204
205 pub async fn shutdown(&self) {
212 self.shutting_down.store(true, Ordering::Release);
213 let handle = { self.envoy.lock().await.take() };
214 #[cfg(feature = "native-runtime")]
215 let development_processes = { self.development_processes.lock().await.take() };
216 let shutdown_envoy = async move {
217 if let Some(handle) = handle {
218 match timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.shutdown_and_wait(false)).await {
219 Ok(()) => {}
220 Err(_) => {
221 tracing::warn!(
222 "serverless runtime envoy drain exceeded timeout; forcing immediate stop"
223 );
224 handle.shutdown(true);
225 handle.wait_stopped().await;
226 }
227 }
228 }
229 };
230 #[cfg(feature = "native-runtime")]
231 let shutdown_development_processes = async move {
232 if let Some(development_processes) = development_processes {
233 development_processes.shutdown().await;
234 }
235 };
236 #[cfg(feature = "native-runtime")]
237 tokio::join!(shutdown_envoy, shutdown_development_processes);
238 #[cfg(not(feature = "native-runtime"))]
239 shutdown_envoy.await;
240 }
241
242 pub async fn wait_actors_drained(&self, timeout_dur: Duration) {
246 let handle = { self.envoy.lock().await.as_ref().cloned() };
247 let Some(handle) = handle else { return };
248 let _ = timeout(
249 timeout_dur,
250 CoreEnvoyHandle::new(handle).wait_actors_drained(),
251 )
252 .await;
253 }
254
255 pub async fn active_envoy_actor_count(&self) -> Option<usize> {
256 self.active_envoy_status()
257 .await
258 .map(|status| status.active_actor_count)
259 }
260
261 pub async fn active_envoy_status(&self) -> Option<CoreEnvoyStatus> {
262 self.envoy
263 .lock()
264 .await
265 .as_ref()
266 .map(|handle| CoreEnvoyHandle::new(handle.clone()).status())
267 }
268
269 pub async fn active_envoy_actor_stop_threshold_ms(&self) -> Option<i64> {
270 let handle = self.envoy.lock().await.as_ref().cloned()?;
271 CoreEnvoyHandle::new(handle).actor_stop_threshold_ms().await
272 }
273
274 pub fn max_request_body_bytes(&self) -> usize {
276 self.settings.max_start_payload_bytes
277 }
278
279 pub fn handles_listener_request(&self, url: &str) -> bool {
283 handles_listener_request(&self.settings.base_path, url)
284 }
285
286 pub fn incoming_too_long_response(&self) -> ServerlessResponse {
288 let error = IncomingMessageTooLong {
289 limit: self.settings.max_start_payload_bytes,
290 }
291 .build();
292 error_response(error)
293 }
294
295 pub fn invalid_request_response(&self, reason: impl Into<String>) -> ServerlessResponse {
297 let error = InvalidRequest {
298 reason: reason.into(),
299 }
300 .build();
301 error_response(error)
302 }
303
304 pub async fn handle_request(&self, req: ServerlessRequest) -> ServerlessResponse {
305 let cors = cors_headers(&req);
306 match self.handle_request_inner(req).await {
307 Ok(mut response) => {
308 apply_cors(&mut response.headers, cors);
309 response
310 }
311 Err(error) => {
312 let mut response = error_response(error);
313 apply_cors(&mut response.headers, cors);
314 response
315 }
316 }
317 }
318
319 async fn handle_request_inner(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
320 let path = route_path(&self.settings.base_path, &req.url)?;
321 match (req.method.as_str(), path.as_str()) {
322 ("GET", "") | ("GET", "/") => Ok(text_response(
323 StatusCode::OK,
324 "text/plain; charset=utf-8",
325 "This is a RivetKit server.\n\nLearn more at https://rivet.dev",
326 )),
327 ("GET", "/health") => {
328 let runtime_healthy = {
333 let guard = self.envoy.lock().await;
334 guard
335 .as_ref()
336 .map(|handle| handle.is_ping_healthy())
337 .unwrap_or(true)
338 };
339 if runtime_healthy {
340 Ok(json_response(
341 StatusCode::OK,
342 json!({
343 "status": "ok",
344 "runtime": "rivetkit",
345 "version": self.settings.package_version,
346 }),
347 ))
348 } else {
349 Ok(json_response(
350 StatusCode::SERVICE_UNAVAILABLE,
351 json!({
352 "status": "engine_ping_stale",
353 "runtime": "rivetkit",
354 "version": self.settings.package_version,
355 }),
356 ))
357 }
358 }
359 ("GET", "/metadata") => Ok(self.metadata_response()),
360 ("GET", "/metrics") => Ok(metrics_response(&req.headers)),
361 ("GET", "/start") | ("POST", "/start") => self.start_response(req).await,
362 ("OPTIONS", _) => Ok(bytes_response(
363 StatusCode::NO_CONTENT,
364 HashMap::new(),
365 Vec::new(),
366 )),
367 _ => Ok(text_response(
368 StatusCode::NOT_FOUND,
369 "text/plain; charset=utf-8",
370 "Not Found (RivetKit)",
371 )),
372 }
373 }
374
375 async fn start_response(&self, req: ServerlessRequest) -> Result<ServerlessResponse> {
376 let headers = parse_start_headers(&req.headers)?;
377 self.validate_start_headers(&headers)?;
378 crate::metrics_endpoint::record_rivetkit_info(
379 self.settings.package_version.clone(),
380 self.settings.version,
381 "serverless",
382 headers.pool_name.clone(),
383 );
384 if req.body.len() > self.settings.max_start_payload_bytes {
385 return Err(IncomingMessageTooLong {
386 limit: self.settings.max_start_payload_bytes,
387 }
388 .build());
389 }
390
391 let handle = self.ensure_envoy(&headers).await?;
392 let payload = req.body;
393 let actor_start = handle.decode_serverless_actor_start(&payload)?;
394 let cancel_token = req.cancel_token;
395 let cache_envoy = self.settings.cache_envoy;
396 let (tx, rx) = mpsc::unbounded_channel();
397 let _ = tx.send(Ok(SSE_PING_FRAME.to_vec()));
398
399 RuntimeSpawner::spawn(async move {
400 let shutdown_handle = handle.clone();
401 let result = tokio::select! {
402 _ = cancel_token.cancelled() => {
403 if !cache_envoy {
404 shutdown_handle.shutdown_and_wait(false).await;
405 }
406 return;
407 }
408 result = handle.start_serverless_actor(&payload) => result,
409 };
410 if let Err(error) = result {
411 let error = stream_error(error);
412 let _ = tx.send(Err(error));
413 if !cache_envoy {
414 handle.shutdown_and_wait(false).await;
415 }
416 return;
417 }
418
419 loop {
420 tokio::select! {
421 _ = cancel_token.cancelled() => {
422 break;
423 }
424 _ = handle.wait_actor_registered_then_stopped(&actor_start.actor_id, actor_start.generation) => {
425 let _ = tx.send(Ok(SSE_STOPPING_FRAME.to_vec()));
426 break;
427 }
428 _ = sleep(SSE_PING_INTERVAL) => {
429 if tx.send(Ok(SSE_PING_FRAME.to_vec())).is_err() {
430 break;
431 }
432 }
433 }
434 }
435
436 if !cache_envoy {
437 handle.shutdown_and_wait(false).await;
438 }
439 });
440
441 Ok(ServerlessResponse {
442 status: StatusCode::OK.as_u16(),
443 headers: HashMap::from([
444 ("content-type".to_owned(), "text/event-stream".to_owned()),
445 ("cache-control".to_owned(), "no-cache".to_owned()),
446 ("connection".to_owned(), "keep-alive".to_owned()),
447 ]),
448 body: rx,
449 })
450 }
451
452 fn metadata_response(&self) -> ServerlessResponse {
453 let actor_names = self
454 .dispatcher
455 .build_actor_metadata_map()
456 .into_iter()
457 .map(|(name, metadata)| {
458 (
459 name,
460 ActorName {
461 metadata: Some(metadata),
462 },
463 )
464 })
465 .collect::<HashMap<_, _>>();
466
467 let payload = ServerlessMetadataPayload {
468 runtime: "rivetkit".to_owned(),
469 version: self.settings.package_version.clone(),
470 envoy_protocol_version: Some(protocol::PROTOCOL_VERSION),
471 actor_names,
472 envoy: Some(ServerlessMetadataEnvoy {
473 kind: Some(ServerlessMetadataEnvoyKind::Serverless {}),
474 version: Some(self.settings.version),
475 }),
476 runner: None,
477 client_endpoint: self.settings.client_endpoint.clone(),
478 client_namespace: self.settings.client_namespace.clone(),
479 client_token: self.settings.client_token.clone(),
480 };
481
482 let response = serde_json::to_value(payload).unwrap_or_else(|_| json!({}));
483
484 json_response(StatusCode::OK, response)
485 }
486
487 fn validate_start_headers(&self, headers: &StartHeaders) -> Result<()> {
488 if self.settings.validate_endpoint {
489 if !endpoints_match(&headers.endpoint, &self.settings.configured_endpoint) {
490 tracing::warn!(
491 configured_endpoint = %self.settings.configured_endpoint,
492 received_endpoint = %headers.endpoint,
493 "serverless start rejected: endpoint mismatch",
494 );
495 return Err(EndpointMismatch {
496 expected: self.settings.configured_endpoint.clone(),
497 received: headers.endpoint.clone(),
498 }
499 .build());
500 }
501
502 if headers.namespace != self.settings.configured_namespace {
503 tracing::warn!(
504 configured_namespace = %self.settings.configured_namespace,
505 received_namespace = %headers.namespace,
506 "serverless start rejected: namespace mismatch",
507 );
508 return Err(NamespaceMismatch {
509 expected: self.settings.configured_namespace.clone(),
510 received: headers.namespace.clone(),
511 }
512 .build());
513 }
514 }
515
516 Ok(())
517 }
518
519 async fn ensure_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
520 if self.shutting_down.load(Ordering::Acquire) {
521 return Err(RuntimeShutDown.build());
522 }
523 if !self.settings.cache_envoy {
524 return self.start_envoy(headers).await;
525 }
526 let mut guard = self.envoy.lock().await;
527 if let Some(handle) = guard.as_ref() {
528 if !endpoints_match(handle.endpoint(), &headers.endpoint)
531 || handle.namespace() != headers.namespace
532 || handle.pool_name() != headers.pool_name
533 {
534 anyhow::bail!("serverless start headers do not match active envoy");
535 }
536 return Ok(handle.clone());
537 }
538
539 let handle = self.start_envoy(headers).await?;
540 if self.shutting_down.load(Ordering::Acquire) {
544 drop(guard);
545 match timeout(SHUTDOWN_DRAIN_TIMEOUT, handle.shutdown_and_wait(false)).await {
546 Ok(()) => {}
547 Err(_) => {
548 handle.shutdown(true);
549 handle.wait_stopped().await;
550 }
551 }
552 return Err(RuntimeShutDown.build());
553 }
554 *guard = Some(handle.clone());
555 Ok(handle)
556 }
557
558 async fn start_envoy(&self, headers: &StartHeaders) -> Result<EnvoyHandle> {
559 let callbacks = Arc::new(RegistryCallbacks {
560 dispatcher: self.dispatcher.clone(),
561 });
562 let prepopulate_actor_names = self
563 .dispatcher
564 .build_actor_metadata_map()
565 .into_iter()
566 .map(|(name, metadata)| (name, EnvoyActorName { metadata }))
567 .collect();
568 Ok(start_envoy_client(EnvoyConfig {
573 version: self.settings.version,
574 endpoint: headers.endpoint.clone(),
575 token: headers.token.clone(),
576 namespace: headers.namespace.clone(),
577 pool_name: headers.pool_name.clone(),
578 prepopulate_actor_names,
579 metadata: Some(json!({
580 "rivetkit": { "version": self.settings.package_version },
581 })),
582 not_global: true,
583 debug_latency_ms: None,
584 callbacks,
585 })
586 .await)
587 }
588}
589
590fn route_path(base_path: &str, url: &str) -> Result<String> {
591 let parsed = Url::parse(url).with_context(|| format!("parse request URL `{url}`"))?;
592 let path = parsed.path();
593 if path == base_path {
594 return Ok(String::new());
595 }
596 let prefix = format!("{base_path}/");
597 if let Some(rest) = path.strip_prefix(&prefix) {
598 return Ok(format!("/{rest}"));
599 }
600 Ok(path.to_owned())
601}
602
603fn handles_listener_request(base_path: &str, url: &str) -> bool {
604 let Ok(parsed) = Url::parse(url) else {
605 return true;
608 };
609 let request_path = parsed.path();
610 if request_path != base_path && !request_path.starts_with(&format!("{base_path}/")) {
611 return false;
612 }
613 let path = route_path(base_path, url).expect("URL was parsed and the same base path is valid");
614 matches!(
615 path.as_str(),
616 "" | "/" | "/health" | "/metadata" | "/metrics" | "/start"
617 )
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 metrics_response(headers: &HashMap<String, String>) -> ServerlessResponse {
730 let bearer_token = crate::metrics_endpoint::authorization_bearer_token_map(headers);
731 match crate::metrics_endpoint::authorize_metrics_request(bearer_token) {
732 Ok(()) => match crate::metrics_endpoint::render_prometheus_metrics() {
733 Ok(metrics) => bytes_response(
734 StatusCode::OK,
735 HashMap::from([("content-type".to_owned(), metrics.content_type)]),
736 metrics.body,
737 ),
738 Err(error) => error_response(error),
739 },
740 Err(crate::metrics_endpoint::MetricsAccessError::NotEnabled) => text_response(
741 StatusCode::FORBIDDEN,
742 "text/plain; charset=utf-8",
743 "metrics not enabled\n",
744 ),
745 Err(crate::metrics_endpoint::MetricsAccessError::Unauthorized) => text_response(
746 StatusCode::UNAUTHORIZED,
747 "text/plain; charset=utf-8",
748 "metrics request requires a valid bearer token\n",
749 ),
750 }
751}
752
753fn bytes_response(
754 status: StatusCode,
755 headers: HashMap<String, String>,
756 body: Vec<u8>,
757) -> ServerlessResponse {
758 let (tx, rx) = mpsc::unbounded_channel();
759 let _ = tx.send(Ok(body));
760 ServerlessResponse {
761 status: status.as_u16(),
762 headers,
763 body: rx,
764 }
765}
766
767fn error_response(error: anyhow::Error) -> ServerlessResponse {
768 let extracted = rivet_error::RivetError::extract(&error);
769 let status = serverless_error_status(extracted.group(), extracted.code());
770 let body = ServerlessErrorBody {
771 group: extracted.group(),
772 code: extracted.code(),
773 message: extracted.message().to_owned(),
774 metadata: extracted.metadata().unwrap_or(serde_json::Value::Null),
775 };
776 bytes_response(
777 status,
778 HashMap::from([("content-type".to_owned(), "application/json".to_owned())]),
779 serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
780 )
781}
782
783fn serverless_error_status(group: &str, code: &str) -> StatusCode {
784 match (group, code) {
785 ("auth", "forbidden") => StatusCode::FORBIDDEN,
786 ("message", "incoming_too_long") => StatusCode::PAYLOAD_TOO_LARGE,
787 _ => StatusCode::BAD_REQUEST,
788 }
789}
790
791fn stream_error(error: anyhow::Error) -> ServerlessStreamError {
792 let extracted = rivet_error::RivetError::extract(&error);
793 ServerlessStreamError {
794 group: extracted.group().to_owned(),
795 code: extracted.code().to_owned(),
796 message: extracted.message().to_owned(),
797 }
798}
799
800pub fn normalize_endpoint_url(url: &str) -> Option<String> {
801 let parsed = Url::parse(url).ok()?;
802 let pathname = if parsed.path() == "/" {
803 "/".to_owned()
804 } else {
805 parsed.path().trim_end_matches('/').to_owned()
806 };
807 let mut hostname = parsed.host_str()?.to_owned();
808 if is_loopback_address(&hostname) {
809 hostname = "localhost".to_owned();
810 }
811 hostname = normalize_regional_hostname(&hostname);
812 let host = match parsed.port() {
813 Some(port) => format!("{hostname}:{port}"),
814 None => hostname,
815 };
816 Some(format!("{}://{}{}", parsed.scheme(), host, pathname))
817}
818
819fn normalized_endpoint_candidates(value: &str) -> Vec<String> {
820 value
821 .split(',')
822 .map(str::trim)
823 .filter(|candidate| !candidate.is_empty())
824 .map(|candidate| normalize_endpoint_url(candidate).unwrap_or_else(|| candidate.to_owned()))
825 .collect()
826}
827
828pub fn endpoints_match(a: &str, b: &str) -> bool {
829 let a_candidates = normalized_endpoint_candidates(a);
830 let b_candidates = normalized_endpoint_candidates(b);
831 a_candidates.iter().any(|a_candidate| {
832 b_candidates
833 .iter()
834 .any(|b_candidate| a_candidate == b_candidate)
835 })
836}
837
838fn normalize_regional_hostname(hostname: &str) -> String {
839 if !hostname.ends_with(".rivet.dev") || !hostname.starts_with("api-") {
840 return hostname.to_owned();
841 }
842 let without_prefix = &hostname[4..];
843 let Some(first_dot_index) = without_prefix.find('.') else {
844 return hostname.to_owned();
845 };
846 let domain = &without_prefix[first_dot_index + 1..];
847 format!("api.{domain}")
848}
849
850fn is_loopback_address(hostname: &str) -> bool {
851 matches!(hostname, "127.0.0.1" | "0.0.0.0" | "::1" | "[::1]")
852}
853
854#[cfg(test)]
856#[path = "../tests/serverless.rs"]
857mod tests;