fix: simplify asset serving, use fallback primarily

This commit is contained in:
2025-09-13 12:23:27 -05:00
parent 9d51fde893
commit a917315967
3 changed files with 35 additions and 50 deletions

View File

@@ -50,15 +50,15 @@ struct Args {
tracing: TracingFormat,
/// Services to run (comma-separated). Default: all services
///
///
/// Examples:
/// --services bot,web # Run only bot and web services
/// --services scraper # Run only the scraper service
#[arg(long, value_delimiter = ',', conflicts_with = "disable_services")]
services: Option<Vec<ServiceName>>,
/// Services to disable (comma-separated)
///
///
/// Examples:
/// --disable-services bot # Run web and scraper only
/// --disable-services bot,web # Run only the scraper service
@@ -89,7 +89,7 @@ impl ServiceName {
fn all() -> Vec<ServiceName> {
vec![ServiceName::Bot, ServiceName::Web, ServiceName::Scraper]
}
/// Convert to string for service registration
fn as_str(&self) -> &'static str {
match self {
@@ -121,7 +121,9 @@ fn determine_enabled_services(args: &Args) -> Result<Vec<ServiceName>, anyhow::E
}
(Some(_), Some(_)) => {
// This should be prevented by clap's conflicts_with, but just in case
Err(anyhow::anyhow!("Cannot specify both --services and --disable-services"))
Err(anyhow::anyhow!(
"Cannot specify both --services and --disable-services"
))
}
}
}

View File

@@ -9,6 +9,7 @@ use axum::{
response::{Html, IntoResponse, Response},
};
use rust_embed::RustEmbed;
use tracing::debug;
/// Embedded web assets from the dist directory
#[derive(RustEmbed)]
@@ -17,36 +18,6 @@ use rust_embed::RustEmbed;
#[exclude = "*.map"]
pub struct WebAssets;
/// Serve embedded static assets
pub async fn serve_asset(Path(path): Path<String>) -> Response {
let path = path.trim_start_matches('/');
match WebAssets::get(path) {
Some(content) => {
let mime_type = mime_guess::from_path(path).first_or_text_plain();
let data = content.data.to_vec();
([(header::CONTENT_TYPE, mime_type.as_ref())], data).into_response()
}
None => (StatusCode::NOT_FOUND, "Asset not found").into_response(),
}
}
/// Serve the main SPA index.html for client-side routing
pub async fn serve_spa_index() -> Response {
match WebAssets::get("index.html") {
Some(content) => {
let data = content.data.to_vec();
let html_content = String::from_utf8_lossy(&data).to_string();
Html(html_content).into_response()
}
None => (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load index.html",
)
.into_response(),
}
}
const ASSET_EXTENSIONS: &[&str] = &[
"js", "css", "png", "jpg", "jpeg", "gif", "svg", "ico", "woff", "woff2", "ttf", "eot",
];

View File

@@ -4,18 +4,19 @@ use axum::{
Router,
extract::State,
http::{StatusCode, Uri},
response::{IntoResponse, Json, Response},
routing::{any, get},
response::{Html, IntoResponse, Json, Response},
routing::get,
};
use http::header;
use serde_json::{Value, json};
use std::sync::Arc;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing::info;
use tracing::{debug, info};
use crate::web::assets::{is_asset_path, serve_asset, serve_spa_index};
use crate::web::assets::WebAssets;
use crate::banner::BannerApi;
@@ -50,7 +51,6 @@ pub fn create_router(state: BannerState) -> Router {
Router::new()
.route("/", get(root))
.nest("/api", api_router)
.route("/assets/{*path}", any(serve_asset))
.fallback(handle_spa_fallback)
.layer(TraceLayer::new_for_http())
}
@@ -79,20 +79,32 @@ async fn root() -> Response {
/// Handles SPA routing by serving index.html for non-API, non-asset requests
async fn handle_spa_fallback(uri: Uri) -> Response {
let path = uri.path();
let path = uri.path().trim_start_matches('/');
// Don't serve index.html for API routes or asset requests
if path.starts_with("/api/") || is_asset_path(path) {
return (StatusCode::NOT_FOUND, "Not Found").into_response();
if let Some(content) = WebAssets::get(path) {
let mime_type = mime_guess::from_path(path).first_or_text_plain();
let data = content.data.to_vec();
return ([(header::CONTENT_TYPE, mime_type.as_ref())], data).into_response();
} else {
// Any assets that are not found should be treated as a 404, not falling back to the SPA index.html
if path.starts_with("assets/") {
return (StatusCode::NOT_FOUND, "Asset not found").into_response();
}
}
// In production, serve embedded index.html for SPA routing
if cfg!(not(debug_assertions)) {
return serve_spa_index().await;
// Fall back to the SPA index.html
match WebAssets::get("index.html") {
Some(content) => {
let data = content.data.to_vec();
let html_content = String::from_utf8_lossy(&data).to_string();
Html(html_content).into_response()
}
None => (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load index.html",
)
.into_response(),
}
// Development fallback (shouldn't reach here in production)
(StatusCode::NOT_FOUND, "Not Found").into_response()
}
/// Health check endpoint