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			// The handler may have registered cancellation listeners on the token.
198			request_token.cancel();
199			(
200				StatusCode::INTERNAL_SERVER_ERROR,
201				[("content-type", "text/plain; charset=utf-8")],
202				"Internal Server Error",
203			)
204				.into_response()
205		}
206	}
207}
208
209async fn forward_request(
210	State(state): State<AppState>,
211	request: Request,
212) -> axum::response::Response {
213	let (parts, body) = request.into_parts();
214	let body_limit = state.runtime.max_request_body_bytes();
215	let request_token = state.shutdown_token.child_token();
216	let body_bytes = match axum::body::to_bytes(body, body_limit).await {
217		Ok(bytes) => bytes,
218		Err(error) if is_length_limit_error(&error) => {
219			tracing::warn!(body_limit, "request body exceeded limit");
220			return into_axum_response(state.runtime.incoming_too_long_response(), request_token);
221		}
222		Err(error) => {
223			tracing::warn!(?error, "failed to read request body");
224			return into_axum_response(
225				state
226					.runtime
227					.invalid_request_response("failed to read request body"),
228				request_token,
229			);
230		}
231	};
232
233	let application_request =
234		application_request_from_parts(parts, body_bytes, request_token.clone());
235	let req = ServerlessRequest {
236		method: application_request.method,
237		url: application_request.url,
238		headers: application_request.headers,
239		body: application_request.body,
240		cancel_token: request_token.clone(),
241	};
242
243	if state.runtime.handles_listener_request(&req.url) || state.application.is_none() {
244		return into_axum_response(state.runtime.handle_request(req).await, request_token);
245	}
246
247	let application = state
248		.application
249		.as_ref()
250		.expect("application checked above");
251	match application(ApplicationRequest {
252		method: req.method,
253		url: req.url,
254		headers: req.headers,
255		body: req.body,
256		cancel_token: request_token.clone(),
257	})
258	.await
259	{
260		Ok(response) => into_application_response(response, request_token),
261		Err(error) => {
262			tracing::error!(?error, "application request handler failed");
263			// The handler may have registered cancellation listeners on the token.
264			request_token.cancel();
265			(
266				StatusCode::INTERNAL_SERVER_ERROR,
267				[("content-type", "text/plain; charset=utf-8")],
268				"Internal Server Error",
269			)
270				.into_response()
271		}
272	}
273}
274
275fn application_request_from_parts(
276	parts: axum::http::request::Parts,
277	body: Bytes,
278	cancel_token: CancellationToken,
279) -> ApplicationRequest {
280	let path_and_query = parts
281		.uri
282		.path_and_query()
283		.map(|pq| pq.as_str())
284		.unwrap_or("/");
285	let forwarded_proto = parts
286		.headers
287		.get("x-forwarded-proto")
288		.and_then(|value| value.to_str().ok())
289		.and_then(|value| value.split(',').next())
290		.map(str::trim)
291		.filter(|value| matches!(*value, "http" | "https"));
292	let forwarded_authority = parts
293		.headers
294		.get("x-forwarded-host")
295		.and_then(|value| value.to_str().ok())
296		.and_then(|value| value.split(',').next())
297		.map(str::trim)
298		.and_then(|value| value.parse::<Authority>().ok());
299	let authority = parts
300		.uri
301		.authority()
302		.cloned()
303		.or(forwarded_authority)
304		.or_else(|| {
305			parts
306				.headers
307				.get(HOST)
308				.and_then(|value| value.to_str().ok())
309				.and_then(|value| value.parse::<Authority>().ok())
310		});
311	let scheme = forwarded_proto
312		.or_else(|| parts.uri.scheme_str())
313		.unwrap_or("http");
314	let url = authority.map_or_else(
315		|| format!("http://internal{path_and_query}"),
316		|authority| format!("{scheme}://{authority}{path_and_query}"),
317	);
318
319	// Repeated header names get comma-joined per RFC 9110 ยง5.3.
320	let mut headers: HashMap<String, String> = HashMap::new();
321	for (name, value) in parts.headers.iter() {
322		let Ok(value_str) = value.to_str() else {
323			continue;
324		};
325		let key = name.as_str().to_ascii_lowercase();
326		headers
327			.entry(key)
328			.and_modify(|existing| {
329				existing.push_str(", ");
330				existing.push_str(value_str);
331			})
332			.or_insert_with(|| value_str.to_owned());
333	}
334
335	ApplicationRequest {
336		method: parts.method.as_str().to_owned(),
337		url,
338		headers,
339		body: body.to_vec(),
340		cancel_token,
341	}
342}
343
344fn into_application_response(
345	response: ApplicationResponse,
346	request_token: CancellationToken,
347) -> axum::response::Response {
348	let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
349	let mut header_map = HeaderMap::with_capacity(response.headers.len());
350	for (name, value) in response.headers {
351		if let (Ok(name), Ok(value)) = (
352			HeaderName::try_from(name.as_str()),
353			HeaderValue::from_str(&value),
354		) {
355			header_map.append(name, value);
356		}
357	}
358	let body = match response.body {
359		ApplicationResponseBody::Buffered(body) => Body::from(body),
360		ApplicationResponseBody::Stream(receiver) => {
361			let stream_token = request_token.clone();
362			let stream =
363				futures::stream::unfold((receiver, false), move |(mut receiver, finished)| {
364					let stream_token = stream_token.clone();
365					async move {
366						if finished {
367							return None;
368						}
369						let next = tokio::select! {
370							_ = stream_token.cancelled() => return None,
371							next = receiver.recv() => next,
372						};
373						match next {
374							Some(ResponseChunk::Data { data, finish }) => {
375								if finish && data.is_empty() {
376									None
377								} else {
378									Some((
379										Ok::<Bytes, std::io::Error>(Bytes::from(data)),
380										(receiver, finish),
381									))
382								}
383							}
384							Some(ResponseChunk::Error(message)) => {
385								Some((Err(std::io::Error::other(message)), (receiver, true)))
386							}
387							None => None,
388						}
389					}
390				});
391			Body::from_stream(stream)
392		}
393	};
394	let guarded = CancelOnDropStream {
395		inner: body.into_data_stream(),
396		_guard: CancelOnDrop {
397			token: request_token,
398		},
399	};
400	(status, header_map, Body::from_stream(guarded)).into_response()
401}
402
403fn into_axum_response(
404	response: ServerlessResponse,
405	request_token: CancellationToken,
406) -> axum::response::Response {
407	let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
408	let mut header_map = HeaderMap::with_capacity(response.headers.len());
409	for (name, value) in response.headers {
410		if let (Ok(name), Ok(value)) = (
411			HeaderName::try_from(name.as_str()),
412			HeaderValue::from_str(&value),
413		) {
414			header_map.append(name, value);
415		}
416	}
417
418	let stream = UnboundedReceiverStream::new(response.body).map(|chunk| match chunk {
419		Ok(bytes) => Ok::<Bytes, std::io::Error>(Bytes::from(bytes)),
420		Err(error) => {
421			tracing::warn!(?error, "serverless stream error");
422			Err(std::io::Error::other(format!(
423				"{}.{}: {}",
424				error.group, error.code, error.message
425			)))
426		}
427	});
428
429	// Cancel the runtime task when the response body is dropped.
430	let guarded = CancelOnDropStream {
431		inner: stream,
432		_guard: CancelOnDrop {
433			token: request_token,
434		},
435	};
436
437	(status, header_map, Body::from_stream(guarded)).into_response()
438}
439
440fn is_length_limit_error(error: &axum::Error) -> bool {
441	let mut source: Option<&dyn std::error::Error> = Some(error);
442	while let Some(err) = source {
443		if err.is::<LengthLimitError>() {
444			return true;
445		}
446		source = err.source();
447	}
448	false
449}
450
451struct CancelOnDrop {
452	token: CancellationToken,
453}
454
455impl Drop for CancelOnDrop {
456	fn drop(&mut self) {
457		self.token.cancel();
458	}
459}
460
461struct CancelOnDropStream<S> {
462	inner: S,
463	_guard: CancelOnDrop,
464}
465
466impl<S: Stream + Unpin> Stream for CancelOnDropStream<S> {
467	type Item = S::Item;
468
469	fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
470		Pin::new(&mut self.inner).poll_next(cx)
471	}
472}