Skip to main content

rivetkit_core/
serverless_http.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context as TaskContext, Poll};
6
7use anyhow::{Context, Result};
8use axum::Router;
9use axum::body::{Body, Bytes};
10use axum::extract::{Request, State};
11use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
12use axum::response::IntoResponse;
13use axum::routing::any;
14use futures::Stream;
15use futures::StreamExt;
16use futures::future::BoxFuture;
17use http_body_util::LengthLimitError;
18use tokio_stream::wrappers::UnboundedReceiverStream;
19use tokio_util::sync::CancellationToken;
20use tower_http::services::ServeDir;
21
22use crate::serverless::{CoreServerlessRuntime, ServerlessRequest, ServerlessResponse};
23
24#[derive(Clone)]
25pub struct ListenerConfig {
26	/// Host to bind; accepts numeric IPs or DNS names. Defaults to `0.0.0.0`.
27	pub host: Option<String>,
28	pub port: u16,
29	pub public_dir: Option<PathBuf>,
30	/// Optional application handler for requests not owned by RivetKit.
31	pub application: Option<ApplicationFetch>,
32}
33
34#[derive(Debug)]
35pub struct ApplicationRequest {
36	pub method: String,
37	pub url: String,
38	pub headers: HashMap<String, String>,
39	pub body: Vec<u8>,
40}
41
42#[derive(Debug)]
43pub struct ApplicationResponse {
44	pub status: u16,
45	pub headers: HashMap<String, String>,
46	pub body: Vec<u8>,
47}
48
49pub type ApplicationFetch = Arc<
50	dyn Fn(ApplicationRequest) -> BoxFuture<'static, Result<ApplicationResponse>> + Send + Sync,
51>;
52
53#[derive(Clone)]
54struct AppState {
55	runtime: CoreServerlessRuntime,
56	application: Option<ApplicationFetch>,
57	shutdown_token: CancellationToken,
58}
59
60#[derive(Clone)]
61struct ApplicationState {
62	application: ApplicationFetch,
63}
64
65/// Bind a TCP listener and serve `runtime` over HTTP until `shutdown` fires.
66pub async fn serve(
67	runtime: CoreServerlessRuntime,
68	listener: ListenerConfig,
69	shutdown: CancellationToken,
70) -> Result<()> {
71	let host = listener.host.as_deref().unwrap_or("0.0.0.0");
72	let port = listener.port;
73
74	let state = AppState {
75		runtime,
76		application: listener.application.clone(),
77		shutdown_token: shutdown.clone(),
78	};
79
80	let forward_service = any(forward_request).with_state(state);
81
82	let router = match listener.public_dir.as_ref() {
83		Some(dir) => Router::new().fallback_service(
84			ServeDir::new(dir)
85				.call_fallback_on_method_not_allowed(true)
86				.fallback(forward_service),
87		),
88		None => Router::new().fallback_service(forward_service),
89	};
90
91	let tcp = tokio::net::TcpListener::bind((host, port))
92		.await
93		.with_context(|| format!("bind tcp listener on {host}:{port}"))?;
94	let bound = tcp
95		.local_addr()
96		.context("read local address of bound listener")?;
97	tracing::info!(host = %bound.ip(), port = bound.port(), "rivetkit server listening");
98
99	let shutdown_fut = {
100		let shutdown = shutdown.clone();
101		async move { shutdown.cancelled().await }
102	};
103
104	axum::serve(tcp, router.into_make_service())
105		.with_graceful_shutdown(shutdown_fut)
106		.await
107		.context("axum::serve returned an error")?;
108
109	Ok(())
110}
111
112/// Bind a TCP listener that forwards every request to an application handler.
113///
114/// This listener is independent of the serverless runtime and can run beside a
115/// normal serverful envoy using the same registry shutdown token.
116pub async fn serve_application(
117	listener: ListenerConfig,
118	application: ApplicationFetch,
119	max_body_bytes: usize,
120	shutdown: CancellationToken,
121) -> Result<()> {
122	let host = listener.host.as_deref().unwrap_or("0.0.0.0");
123	let port = listener.port;
124	let forward_service = any(forward_application_request)
125		.with_state((ApplicationState { application }, max_body_bytes));
126	let router = match listener.public_dir.as_ref() {
127		Some(dir) => Router::new().fallback_service(
128			ServeDir::new(dir)
129				.call_fallback_on_method_not_allowed(true)
130				.fallback(forward_service),
131		),
132		None => Router::new().fallback_service(forward_service),
133	};
134	let tcp = tokio::net::TcpListener::bind((host, port))
135		.await
136		.with_context(|| format!("bind application tcp listener on {host}:{port}"))?;
137	let bound = tcp
138		.local_addr()
139		.context("read application listener local address")?;
140	tracing::info!(host = %bound.ip(), port = bound.port(), "application server listening");
141
142	axum::serve(tcp, router.into_make_service())
143		.with_graceful_shutdown(async move { shutdown.cancelled().await })
144		.await
145		.context("application axum::serve returned an error")?;
146	Ok(())
147}
148
149async fn forward_application_request(
150	State((state, max_body_bytes)): State<(ApplicationState, usize)>,
151	request: Request,
152) -> axum::response::Response {
153	let (parts, body) = request.into_parts();
154	let body = match axum::body::to_bytes(body, max_body_bytes).await {
155		Ok(body) => body,
156		Err(error) if is_length_limit_error(&error) => {
157			tracing::warn!(max_body_bytes, "application request body exceeded limit");
158			return (
159				StatusCode::PAYLOAD_TOO_LARGE,
160				[("content-type", "text/plain; charset=utf-8")],
161				"Payload Too Large",
162			)
163				.into_response();
164		}
165		Err(error) => {
166			tracing::warn!(?error, "failed to read application request body");
167			return (
168				StatusCode::BAD_REQUEST,
169				[("content-type", "text/plain; charset=utf-8")],
170				"Bad Request",
171			)
172				.into_response();
173		}
174	};
175	let request = application_request_from_parts(parts, body);
176	match (state.application)(request).await {
177		Ok(response) => into_application_response(response),
178		Err(error) => {
179			tracing::error!(?error, "application request handler failed");
180			(
181				StatusCode::INTERNAL_SERVER_ERROR,
182				[("content-type", "text/plain; charset=utf-8")],
183				"Internal Server Error",
184			)
185				.into_response()
186		}
187	}
188}
189
190async fn forward_request(
191	State(state): State<AppState>,
192	request: Request,
193) -> axum::response::Response {
194	let (parts, body) = request.into_parts();
195	let body_limit = state.runtime.max_request_body_bytes();
196	let request_token = state.shutdown_token.child_token();
197	let body_bytes = match axum::body::to_bytes(body, body_limit).await {
198		Ok(bytes) => bytes,
199		Err(error) if is_length_limit_error(&error) => {
200			tracing::warn!(body_limit, "request body exceeded limit");
201			return into_axum_response(state.runtime.incoming_too_long_response(), request_token);
202		}
203		Err(error) => {
204			tracing::warn!(?error, "failed to read request body");
205			return into_axum_response(
206				state
207					.runtime
208					.invalid_request_response("failed to read request body"),
209				request_token,
210			);
211		}
212	};
213
214	let application_request = application_request_from_parts(parts, body_bytes);
215	let req = ServerlessRequest {
216		method: application_request.method,
217		url: application_request.url,
218		headers: application_request.headers,
219		body: application_request.body,
220		cancel_token: request_token.clone(),
221	};
222
223	if state.runtime.handles_listener_request(&req.url) || state.application.is_none() {
224		return into_axum_response(state.runtime.handle_request(req).await, request_token);
225	}
226
227	let application = state
228		.application
229		.as_ref()
230		.expect("application checked above");
231	match application(ApplicationRequest {
232		method: req.method,
233		url: req.url,
234		headers: req.headers,
235		body: req.body,
236	})
237	.await
238	{
239		Ok(response) => into_application_response(response),
240		Err(error) => {
241			tracing::error!(?error, "application request handler failed");
242			(
243				StatusCode::INTERNAL_SERVER_ERROR,
244				[("content-type", "text/plain; charset=utf-8")],
245				"Internal Server Error",
246			)
247				.into_response()
248		}
249	}
250}
251
252fn application_request_from_parts(
253	parts: axum::http::request::Parts,
254	body: Bytes,
255) -> ApplicationRequest {
256	let path_and_query = parts
257		.uri
258		.path_and_query()
259		.map(|pq| pq.as_str())
260		.unwrap_or("/");
261	let url = format!("http://internal{path_and_query}");
262
263	// Repeated header names get comma-joined per RFC 9110 ยง5.3.
264	let mut headers: HashMap<String, String> = HashMap::new();
265	for (name, value) in parts.headers.iter() {
266		let Ok(value_str) = value.to_str() else {
267			continue;
268		};
269		let key = name.as_str().to_ascii_lowercase();
270		headers
271			.entry(key)
272			.and_modify(|existing| {
273				existing.push_str(", ");
274				existing.push_str(value_str);
275			})
276			.or_insert_with(|| value_str.to_owned());
277	}
278
279	ApplicationRequest {
280		method: parts.method.as_str().to_owned(),
281		url,
282		headers,
283		body: body.to_vec(),
284	}
285}
286
287fn into_application_response(response: ApplicationResponse) -> axum::response::Response {
288	let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
289	let mut header_map = HeaderMap::with_capacity(response.headers.len());
290	for (name, value) in response.headers {
291		if let (Ok(name), Ok(value)) = (
292			HeaderName::try_from(name.as_str()),
293			HeaderValue::from_str(&value),
294		) {
295			header_map.append(name, value);
296		}
297	}
298	(status, header_map, response.body).into_response()
299}
300
301fn into_axum_response(
302	response: ServerlessResponse,
303	request_token: CancellationToken,
304) -> axum::response::Response {
305	let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
306	let mut header_map = HeaderMap::with_capacity(response.headers.len());
307	for (name, value) in response.headers {
308		if let (Ok(name), Ok(value)) = (
309			HeaderName::try_from(name.as_str()),
310			HeaderValue::from_str(&value),
311		) {
312			header_map.append(name, value);
313		}
314	}
315
316	let stream = UnboundedReceiverStream::new(response.body).map(|chunk| match chunk {
317		Ok(bytes) => Ok::<Bytes, std::io::Error>(Bytes::from(bytes)),
318		Err(error) => {
319			tracing::warn!(?error, "serverless stream error");
320			Err(std::io::Error::other(format!(
321				"{}.{}: {}",
322				error.group, error.code, error.message
323			)))
324		}
325	});
326
327	// Cancel the runtime task when the response body is dropped.
328	let guarded = CancelOnDropStream {
329		inner: stream,
330		_guard: CancelOnDrop {
331			token: request_token,
332		},
333	};
334
335	(status, header_map, Body::from_stream(guarded)).into_response()
336}
337
338fn is_length_limit_error(error: &axum::Error) -> bool {
339	let mut source: Option<&dyn std::error::Error> = Some(error);
340	while let Some(err) = source {
341		if err.is::<LengthLimitError>() {
342			return true;
343		}
344		source = err.source();
345	}
346	false
347}
348
349struct CancelOnDrop {
350	token: CancellationToken,
351}
352
353impl Drop for CancelOnDrop {
354	fn drop(&mut self) {
355		self.token.cancel();
356	}
357}
358
359struct CancelOnDropStream<S> {
360	inner: S,
361	_guard: CancelOnDrop,
362}
363
364impl<S: Stream + Unpin> Stream for CancelOnDropStream<S> {
365	type Item = S::Item;
366
367	fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
368		Pin::new(&mut self.inner).poll_next(cx)
369	}
370}