use axum::{
http::{header, Method, StatusCode, Uri},
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use ferrox_api::routes;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "static/"]
struct Assets;
const INDEX: &str = "index.html";
const API_PREFIXES: &[&str] = &["/v1/", "/admin/", "/health", "/metrics", "/cache/"];
pub(crate) fn attach<S>(router: Router<S>) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
router
.route(routes::ROOT, get(shell))
.route(routes::UI, get(shell))
.fallback(fallback)
}
async fn shell() -> Response {
serve(INDEX).unwrap_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"the embedded UI is missing from this build",
)
.into_response()
})
}
async fn fallback(method: Method, uri: Uri) -> Response {
let path = uri.path();
if method != Method::GET && method != Method::HEAD {
return not_found(path);
}
if let Some(asset) = serve(path.trim_start_matches('/')) {
return asset;
}
if is_api_path(path) || looks_like_a_file(path) {
return not_found(path);
}
shell().await
}
fn serve(relative: &str) -> Option<Response> {
let file = Assets::get(relative)?;
Some(
(
[
(header::CONTENT_TYPE, content_type(relative)),
(header::CACHE_CONTROL, "no-cache"),
],
file.data.into_owned(),
)
.into_response(),
)
}
fn content_type(path: &str) -> &'static str {
match path.rsplit_once('.').map(|(_, ext)| ext) {
Some("html") => "text/html; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("js") | Some("mjs") => "text/javascript; charset=utf-8",
Some("json") | Some("map") => "application/json",
Some("svg") => "image/svg+xml",
Some("png") => "image/png",
Some("ico") => "image/x-icon",
Some("woff2") => "font/woff2",
_ => "application/octet-stream",
}
}
fn is_api_path(path: &str) -> bool {
API_PREFIXES.iter().any(|p| path.starts_with(p))
}
fn looks_like_a_file(path: &str) -> bool {
path.rsplit('/').next().is_some_and(|seg| seg.contains('.'))
}
fn not_found(path: &str) -> Response {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": {
"message": format!("no route for {path}"),
"type": "not_found",
}
})),
)
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::{to_bytes, Body};
use axum::http::Request;
use tower::ServiceExt;
fn app() -> Router {
attach(
Router::new()
.route(
routes::V1_MODELS,
get(|| async { Json(serde_json::json!({"object": "list", "data": []})) }),
)
.route(
routes::ADMIN_STATS,
get(|| async { Json(serde_json::json!({"requests_total": 0})) }),
),
)
}
async fn get_path(path: &str) -> (StatusCode, String, String) {
let response = app()
.oneshot(Request::builder().uri(path).body(Body::empty()).unwrap())
.await
.unwrap();
let status = response.status();
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.map(|v| v.to_str().unwrap().to_string())
.unwrap_or_default();
let body = to_bytes(response.into_body(), 4 << 20).await.unwrap();
(status, content_type, String::from_utf8_lossy(&body).into())
}
#[tokio::test]
async fn the_shell_is_served_at_both_root_and_ui() {
for path in [routes::ROOT, routes::UI] {
let (status, content_type, body) = get_path(path).await;
assert_eq!(status, StatusCode::OK, "{path}");
assert_eq!(content_type, "text/html; charset=utf-8", "{path}");
assert!(body.contains("<!DOCTYPE html>"), "{path} served {body:.80}");
assert!(body.contains("/app.js"), "{path} does not load the app");
}
}
#[tokio::test]
async fn assets_are_served_with_the_content_type_a_browser_needs() {
let (status, content_type, body) = get_path("/app.css").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(content_type, "text/css; charset=utf-8");
assert!(body.contains("--"), "expected CSS custom properties");
let (status, content_type, body) = get_path("/app.js").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(content_type, "text/javascript; charset=utf-8");
assert!(body.contains("import"), "expected an ES module");
}
#[tokio::test]
async fn a_client_side_route_falls_back_to_the_shell() {
let (_, _, shell) = get_path(routes::UI).await;
for path in ["/ui/chat", "/ui/models", "/ui/activity", "/ui/connect"] {
let (status, content_type, body) = get_path(path).await;
assert_eq!(status, StatusCode::OK, "{path}");
assert_eq!(content_type, "text/html; charset=utf-8", "{path}");
assert_eq!(body, shell, "{path} served something other than the shell");
}
}
#[tokio::test]
async fn api_routes_still_win_over_the_fallback() {
let (status, content_type, body) = get_path(routes::V1_MODELS).await;
assert_eq!(status, StatusCode::OK);
assert!(
content_type.starts_with("application/json"),
"{content_type}"
);
assert!(body.contains("\"object\":\"list\""), "{body}");
let (status, _, body) = get_path(routes::ADMIN_STATS).await;
assert_eq!(status, StatusCode::OK);
assert!(body.contains("requests_total"), "{body}");
}
#[tokio::test]
async fn an_unknown_api_path_stays_a_json_404() {
for path in ["/v1/rerank", "/admin/nope", "/metrics/extra"] {
let (status, content_type, body) = get_path(path).await;
assert_eq!(status, StatusCode::NOT_FOUND, "{path}");
assert!(content_type.starts_with("application/json"), "{path}");
assert!(body.contains("not_found"), "{path} -> {body}");
}
}
#[tokio::test]
async fn a_missing_asset_is_a_404_rather_than_the_shell() {
let (status, _, _) = get_path("/nope.js").await;
assert_eq!(status, StatusCode::NOT_FOUND);
let (status, _, _) = get_path("/assets/missing.css").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn a_non_get_request_is_never_answered_with_the_shell() {
let response = app()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/ui/chat")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn every_api_route_is_shielded_from_the_spa_fallback() {
for route in routes::ALL {
if *route == routes::ROOT || *route == routes::UI {
continue;
}
assert!(
is_api_path(route),
"{route} is not covered by API_PREFIXES, so a 404 for it \
would be answered with the HTML shell"
);
}
assert!(is_api_path(&routes::admin_task_cancel("t1")));
}
#[test]
fn the_shell_and_every_module_it_needs_are_embedded() {
let embedded: Vec<String> = Assets::iter().map(|f| f.to_string()).collect();
for required in [
"index.html",
"app.css",
"app.js",
"api.js",
"dom.js",
"md.js",
"chat.js",
"models.js",
"activity.js",
"connect.js",
] {
assert!(
embedded.iter().any(|f| f == required),
"{required} is not embedded; have {embedded:?}"
);
}
}
#[test]
fn only_a_named_extension_gets_a_typed_content_type() {
assert_eq!(content_type("index.html"), "text/html; charset=utf-8");
assert_eq!(content_type("app.js"), "text/javascript; charset=utf-8");
assert_eq!(content_type("noextension"), "application/octet-stream");
}
#[test]
fn a_path_without_a_dotted_last_segment_is_a_route_not_a_file() {
assert!(!looks_like_a_file("/ui/models"));
assert!(!looks_like_a_file("/ui"));
assert!(looks_like_a_file("/app.css"));
assert!(!looks_like_a_file("/v1.5/chat"));
}
}