diff --git a/web/frontend-server/src/main.rs b/web/frontend-server/src/main.rs index 4a8bf6a..e55427b 100644 --- a/web/frontend-server/src/main.rs +++ b/web/frontend-server/src/main.rs @@ -1,16 +1,13 @@ //! Normogen frontend server — a minimal Axum app that: //! -//! 1. Serves the built Vite SPA bundle from `dist/` (static files). -//! 2. Falls back to `index.html` for any unmatched route (SPA history mode). -//! 3. Reverse-proxies `/api/*` requests to the backend container. -//! -//! Designed to run as a standalone container, independently scalable. -//! No business logic, no DB — just static serving + proxying. +//! 1. Reverse-proxies `/api/*` requests to the backend container. +//! 2. Serves the built Vite SPA bundle from `dist/` (static files). +//! 3. Falls back to `index.html` for any unmatched route (SPA history mode). use axum::{ body::Body, extract::{Request, State}, - http::{HeaderValue, Method, StatusCode, Uri}, + http::StatusCode, response::{IntoResponse, Response}, routing::any, Router, @@ -19,7 +16,6 @@ use std::env; use tower_http::services::{ServeDir, ServeFile}; use tower_http::trace::TraceLayer; -/// Shared state: the backend URL + a reqwest client for proxying. #[derive(Clone)] struct ProxyState { backend_url: String, @@ -48,8 +44,9 @@ async fn main() { let serve_dir = ServeDir::new("dist").fallback(ServeFile::new("dist/index.html")); let app = Router::new() - // Proxy /api/* to the backend. - .route("/api/{*rest}", any(proxy_to_backend)) + // Proxy /api and everything under /api/ to the backend. + .route("/api", any(proxy_to_backend)) + .route("/api/*path", any(proxy_to_backend)) .with_state(state) // Everything else: static files + SPA fallback. .fallback_service(serve_dir) @@ -68,32 +65,28 @@ async fn main() { /// query string. Return the backend's response. async fn proxy_to_backend( State(state): State, - mut req: Request, + req: Request, ) -> Response { - // Build the target URL: backend_url + original path + query string. let path = req.uri().path(); let query = req.uri().query().map(|q| format!("?{q}")).unwrap_or_default(); let target_url = format!("{}{}{}", state.backend_url, path, query); - // Map the axum request into a reqwest request. let method = reqwest::Method::from_bytes(req.method().as_str().as_bytes()) .unwrap_or(reqwest::Method::GET); - // Collect headers (skip host — reqwest sets its own). let mut headers = reqwest::header::HeaderMap::new(); for (name, value) in req.headers().iter() { if name == "host" { continue; } - if let (Ok(name), Ok(value)) = ( + if let (Ok(n), Ok(v)) = ( reqwest::header::HeaderName::from_bytes(name.as_str().as_bytes()), reqwest::header::HeaderValue::from_bytes(value.as_bytes()), ) { - headers.insert(name, value); + headers.insert(n, v); } } - // Collect the body. let body_bytes = axum::body::to_bytes(req.into_body(), usize::MAX) .await .unwrap_or_default(); @@ -108,15 +101,10 @@ async fn proxy_to_backend( Ok(r) => r, Err(e) => { eprintln!("Proxy error to {target_url}: {e}"); - return ( - StatusCode::BAD_GATEWAY, - "Failed to reach backend", - ) - .into_response(); + return (StatusCode::BAD_GATEWAY, "Failed to reach backend").into_response(); } }; - // Map the reqwest response back into an axum response. let status = StatusCode::from_u16(backend_resp.status().as_u16()) .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);