use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::service::Service;
use tokio::sync::{watch, RwLock};
use super::router::Router;
use crate::{Error, FlatbedConfig, HeaderMap, HeaderValue, Method, RequestParts, ResponseParts};
#[cfg(feature = "openapi")]
use crate::{get_latest_version, get_openapi_json_for_version, get_route_versions};
pub struct ServiceContext<C> {
pub router: Arc<Router>,
pub healthz_rx: watch::Receiver<bool>,
pub ready_rx: watch::Receiver<bool>,
pub context: Arc<RwLock<Option<Arc<C>>>>,
pub config: FlatbedConfig,
pub static_routes: Arc<Vec<crate::StaticRouteInfo>>,
}
impl<C> ServiceContext<C> {
pub fn is_healthy(&self) -> bool {
*self.healthz_rx.borrow()
}
pub fn is_ready(&self) -> bool {
*self.ready_rx.borrow()
}
}
impl<C> Clone for ServiceContext<C> {
fn clone(&self) -> Self {
Self {
router: Arc::clone(&self.router),
healthz_rx: self.healthz_rx.clone(),
ready_rx: self.ready_rx.clone(),
context: Arc::clone(&self.context),
config: self.config.clone(),
static_routes: Arc::clone(&self.static_routes),
}
}
}
pub struct FlatbedService<C> {
ctx: ServiceContext<C>,
}
impl<C> FlatbedService<C> {
pub fn new(ctx: ServiceContext<C>) -> Self {
Self { ctx }
}
}
impl<C> Clone for FlatbedService<C> {
fn clone(&self) -> Self {
Self {
ctx: self.ctx.clone(),
}
}
}
impl<C: Clone + Send + Sync + 'static> Service<Request<Incoming>> for FlatbedService<C> {
type Response = Response<Full<Bytes>>;
type Error = Infallible;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
let ctx = self.ctx.clone();
Box::pin(async move { Ok(handle_request(req, ctx).await) })
}
}
async fn handle_request<C: Clone + Send + Sync + 'static>(
req: Request<Incoming>,
ctx: ServiceContext<C>,
) -> Response<Full<Bytes>> {
let method = req.method().as_str().to_string();
let path = req.uri().path().to_string();
let query = req.uri().query().map(|s| s.to_string());
if let Some(response) = handle_splash_endpoint(&method, &path, &ctx.config) {
return response;
}
#[cfg(feature = "telemetry")]
if ctx.config.telemetry.is_some() {
if let Some(response) = handle_telemetry_endpoint(&method, &path, &ctx) {
return response;
}
}
#[cfg(feature = "openapi")]
if let Some(response) = handle_openapi_endpoint(&method, &path, &ctx.config) {
return response;
}
if !ctx.is_ready() {
return build_error_response(
StatusCode::SERVICE_UNAVAILABLE,
"BOOTING",
"Server is starting up, please retry shortly",
);
}
let Some((route_entry, path_params)) = ctx.router.match_route(&path, &method) else {
let allowed = ctx.router.get_allowed_methods(&path);
if !allowed.is_empty() {
return build_method_not_allowed(&allowed);
}
if is_get_or_head(&method) && !ctx.static_routes.is_empty() {
if let Some(parts) = super::static_files::serve(&ctx.static_routes, &path).await {
return build_success_response(parts);
}
}
return build_not_found();
};
let content_type = req
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let is_json = content_type.contains("application/json");
let is_flatbuffer = content_type.contains("application/x-flatbuffers")
|| content_type.contains("application/x-flat-buffers");
let needs_body = matches!(method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH");
if needs_body && !is_json && !is_flatbuffer {
return build_unsupported_media_type();
}
let mut headers = HeaderMap::new();
for (key, value) in req.headers().iter() {
if let Ok(val) = HeaderValue::try_from(value.as_bytes()) {
if let Ok(name) = http::header::HeaderName::try_from(key.as_str()) {
headers.insert(name, val);
}
}
}
let body_bytes = match req.into_body().collect().await {
Ok(collected) => collected.to_bytes().to_vec(),
Err(e) => {
return build_error_response(
StatusCode::BAD_REQUEST,
"BODY_READ_ERROR",
&format!("Failed to read request body: {}", e),
);
}
};
let mut request_parts = RequestParts::new(
Method::from_bytes(method.as_bytes()).unwrap_or(Method::POST),
path.clone(),
);
request_parts.headers = headers;
request_parts.path_params = path_params;
if let Some(query) = query {
request_parts.query_params = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let key = parts.next()?;
let value = parts.next().unwrap_or("");
if key.is_empty() {
None
} else {
Some((key.to_string(), value.to_string()))
}
})
.collect();
}
request_parts = request_parts.with_request_id_from_header();
let app_ctx: Arc<dyn std::any::Any + Send + Sync> = {
let guard = ctx.context.read().await;
match guard.as_ref() {
Some(c) => c.clone(),
None => Arc::new(()),
}
};
let handler = route_entry.handler;
match handler(request_parts.clone(), body_bytes, &content_type, app_ctx).await {
Ok(response_parts) => build_success_response(response_parts),
Err(e) => {
let (status, code) = match &e {
Error::DeserializationError(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST"),
Error::SerializationError(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, "SERIALIZATION_ERROR")
}
Error::HandlerError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "HANDLER_ERROR"),
Error::Custom(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"),
};
build_error_response(status, code, &e.to_string())
}
}
}
fn is_get_or_head(method: &str) -> bool {
method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("HEAD")
}
fn handle_splash_endpoint(
method: &str,
path: &str,
config: &FlatbedConfig,
) -> Option<Response<Full<Bytes>>> {
if !is_get_or_head(method) || path != "/" {
return None;
}
let splash = config.splash.as_ref()?;
Some(
Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/plain; charset=utf-8")
.body(Full::new(Bytes::from(splash.clone())))
.unwrap(),
)
}
#[cfg(feature = "telemetry")]
fn handle_telemetry_endpoint<C>(
method: &str,
path: &str,
ctx: &ServiceContext<C>,
) -> Option<Response<Full<Bytes>>> {
if !is_get_or_head(method) {
return None;
}
let telemetry = ctx.config.telemetry.as_ref()?;
match path {
"/healthz" => {
if ctx.is_healthy() {
Some(build_text_response(StatusCode::OK, "OK"))
} else {
Some(build_text_response(
StatusCode::SERVICE_UNAVAILABLE,
"Not Healthy",
))
}
}
"/readyz" => {
if ctx.is_ready() {
Some(build_text_response(StatusCode::OK, "Ready"))
} else {
Some(build_text_response(
StatusCode::SERVICE_UNAVAILABLE,
"Not Ready",
))
}
}
"/metrics" => match telemetry.get_feed() {
Ok(feed) => Some(build_metrics_response(feed)),
Err(e) => Some(build_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"METRICS_ERROR",
&e.to_string(),
)),
},
_ => None,
}
}
#[cfg(feature = "openapi")]
fn handle_openapi_endpoint(
method: &str,
path: &str,
config: &FlatbedConfig,
) -> Option<Response<Full<Bytes>>> {
if !is_get_or_head(method) {
return None;
}
if path == "/openapi.json" {
let version = get_latest_version();
let json = get_openapi_json_for_version(config, &version);
return Some(build_json_response(json));
}
let versions = get_route_versions();
for version in &versions {
let versioned_path = format!("/{}/openapi.json", version);
if path == versioned_path {
let json = get_openapi_json_for_version(config, version);
return Some(build_json_response(json));
}
}
None
}
fn build_success_response(parts: ResponseParts) -> Response<Full<Bytes>> {
let mut builder = Response::builder().status(parts.status);
builder = builder.header("content-type", parts.content_type.as_ref());
for (key, value) in parts.headers.iter() {
if let Ok(val) = value.to_str() {
builder = builder.header(key.as_str(), val);
}
}
builder
.body(Full::new(Bytes::from(parts.body)))
.unwrap_or_else(|_| {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from("Failed to build response")))
.unwrap()
})
}
fn build_not_found() -> Response<Full<Bytes>> {
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("content-type", "text/plain")
.body(Full::new(Bytes::from("Not Found")))
.unwrap()
}
fn build_method_not_allowed(allowed: impl AsRef<[String]>) -> Response<Full<Bytes>> {
Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.header("content-type", "text/plain")
.header("allow", allowed.as_ref().join(", "))
.body(Full::new(Bytes::from("Method Not Allowed")))
.unwrap()
}
fn build_unsupported_media_type() -> Response<Full<Bytes>> {
Response::builder()
.status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(
"Content-Type must be application/json or application/x-flatbuffers",
)))
.unwrap()
}
fn build_error_response(status: StatusCode, code: &str, message: &str) -> Response<Full<Bytes>> {
let body = serde_json::json!({
"code": code,
"message": message
});
Response::builder()
.status(status)
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body.to_string())))
.unwrap()
}
#[cfg(feature = "telemetry")]
fn build_text_response(status: StatusCode, body: &str) -> Response<Full<Bytes>> {
Response::builder()
.status(status)
.header("content-type", "text/plain")
.body(Full::new(Bytes::from(body.to_string())))
.unwrap()
}
#[cfg(feature = "telemetry")]
fn build_metrics_response(metrics: String) -> Response<Full<Bytes>> {
Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/plain; version=0.0.4; charset=utf-8")
.body(Full::new(Bytes::from(metrics)))
.unwrap()
}
#[cfg(feature = "openapi")]
fn build_json_response(json: String) -> Response<Full<Bytes>> {
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Full::new(Bytes::from(json)))
.unwrap()
}