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