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