#include "http_server.hpp" #include "logging/logger.hpp" #include namespace smartbotic::webserver { HttpServer::HttpServer(const HttpServerConfig& config) : config_(config) { // Configure server server_.set_payload_max_length(1024 * 1024 * 16); // 16MB max payload // Compression is auto-enabled when supported by client // Set keep-alive server_.set_keep_alive_max_count(100); server_.set_keep_alive_timeout(30); // Error handler - only set error content if no content already set server_.set_error_handler([](const httplib::Request& req, httplib::Response& res) { // Don't overwrite if content already set by route handler if (!res.body.empty()) { return; } nlohmann::json error; error["error"] = "Internal server error"; error["status"] = res.status; res.set_content(error.dump(), "application/json"); }); // Exception handler server_.set_exception_handler([](const httplib::Request& req, httplib::Response& res, std::exception_ptr ep) { try { std::rethrow_exception(ep); } catch (const std::exception& e) { LOG_ERROR("Unhandled exception: {}", e.what()); nlohmann::json error; error["error"] = "Internal server error"; res.status = 500; res.set_content(error.dump(), "application/json"); } }); // Logger server_.set_logger([](const httplib::Request& req, const httplib::Response& res) { LOG_DEBUG("{} {} {} {}", req.method, req.path, res.status, res.get_header_value("Content-Length")); }); } HttpServer::~HttpServer() { stop(); } void HttpServer::start() { if (running_) { return; } // Setup static files setupStaticFiles(); running_ = true; server_thread_ = std::thread([this] { LOG_INFO("HTTP server starting on port {}", config_.port); if (!server_.listen("0.0.0.0", config_.port)) { LOG_ERROR("Failed to start HTTP server on port {}", config_.port); } }); } void HttpServer::stop() { if (!running_) { return; } running_ = false; server_.stop(); if (server_thread_.joinable()) { server_thread_.join(); } LOG_INFO("HTTP server stopped"); } void HttpServer::registerRoute(const std::string& method, const std::string& pattern, auth::AuthMiddleware::Handler handler, auth::AuthMiddleware* auth, const std::string& required_role) { auto wrapped_handler = [handler, auth, required_role](const httplib::Request& req, httplib::Response& res) { if (auth) { if (!required_role.empty()) { auth->requireRole(req, res, required_role, handler); } else { auth->requireAuth(req, res, handler); } } else { auth::AuthContext ctx; handler(req, res, ctx); } }; if (method == "GET") { server_.Get(pattern, wrapped_handler); } else if (method == "POST") { server_.Post(pattern, wrapped_handler); } else if (method == "PUT") { server_.Put(pattern, wrapped_handler); } else if (method == "PATCH") { server_.Patch(pattern, wrapped_handler); } else if (method == "DELETE") { server_.Delete(pattern, wrapped_handler); } } void HttpServer::setupStaticFiles() { std::filesystem::path static_path = config_.static_files_path; if (!std::filesystem::exists(static_path)) { LOG_WARN("Static files directory not found: {}", static_path.string()); return; } // Serve static files with mount point as fallback server_.set_mount_point("/", static_path.string()); // SPA fallback - catch-all route for client-side routing // Note: This handler serves index.html for SPA routes (paths without extensions) // Static files (with extensions) are handled by the mount point above std::filesystem::path index_path = static_path / "index.html"; if (std::filesystem::exists(index_path)) { server_.Get(R"(/[^.]*$)", [index_path](const httplib::Request& req, httplib::Response& res) { // Don't intercept API, webhook, health, or WebSocket paths if (req.path.starts_with("/api") || req.path.starts_with("/webhook") || req.path.starts_with("/ws") || req.path == "/health") { res.status = 404; res.set_content(R"({"error":"Not found"})", "application/json"); return; } // Read index.html fresh on each request (no caching) std::ifstream file(index_path); if (!file.is_open()) { res.status = 500; res.set_content(R"({"error":"Failed to read index.html"})", "application/json"); return; } std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); // Prevent browser caching of index.html res.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); res.set_header("Pragma", "no-cache"); res.set_header("Expires", "0"); res.set_content(content, "text/html"); }); } LOG_INFO("Static files mounted from: {}", static_path.string()); } } // namespace smartbotic::webserver