diff --git a/include/glaze/net/http_router.hpp b/include/glaze/net/http_router.hpp index 511ae36b48..a7cbef843e 100644 --- a/include/glaze/net/http_router.hpp +++ b/include/glaze/net/http_router.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -518,29 +519,71 @@ namespace glz * @param handle The handler function to call when this route matches * @param spec Optional spec for the route. * @return Reference to this router for method chaining - * @throws std::runtime_error if there's a route conflict + * @throws std::runtime_error if there's a route conflict (exceptions-enabled builds) */ inline basic_http_router& route(http_method method, std::string_view path, handler handle, const route_spec& spec = {}) { + std::string route_error{}; + if (!try_route(method, path, std::move(handle), spec, &route_error)) { +#if __cpp_exceptions + throw std::runtime_error(route_error.empty() ? "Route registration failed" : route_error); +#endif + } + return *this; + } + + /** + * @brief Register a route and report errors without exceptions + * + * @param method The HTTP method (GET, POST, etc.) + * @param path The route path, can include parameters (":param") and wildcards ("*param") + * @param handle The handler function to call when this route matches + * @param spec Optional spec for the route + * @param error_message Optional output for route registration failures + * @return true if registration succeeded, false otherwise + */ + [[nodiscard]] inline bool try_route(http_method method, std::string_view path, handler handle, + const route_spec& spec = {}, std::string* error_message = nullptr) + { + last_route_error.clear(); std::string path_str(path); - try { - // Store in the routes map - auto& entry = routes[path_str][method]; - entry.handle = std::move(handle); - entry.spec = spec; - - // Also add to the radix tree - add_route(method, path, entry.handle, spec.constraints); + + // Store in the routes map first (used for compatibility with mount functionality). + auto& entry = routes[path_str][method]; + entry.handle = std::move(handle); + entry.spec = spec; + + std::string route_error{}; + if (!add_route(method, path, entry.handle, spec.constraints, &route_error)) { + // Keep map/tree consistent on failure. + auto it = routes.find(path_str); + if (it != routes.end()) { + it->second.erase(method); + if (it->second.empty()) { + routes.erase(it); + } + } + + last_route_error = route_error; + if (error_message) { + *error_message = route_error; + } + return false; } - catch (const std::exception& e) { - // Log the error instead of propagating it - std::fprintf(stderr, "Error adding route '%.*s': %s\n", static_cast(path.length()), path.data(), - e.what()); + + if (error_message) { + error_message->clear(); } - return *this; + return true; } + [[nodiscard]] bool has_route_error() const noexcept { return !last_route_error.empty(); } + + [[nodiscard]] std::string_view route_error() const noexcept { return last_route_error; } + + void clear_route_error() noexcept { last_route_error.clear(); } + /** * @brief Register a GET route */ @@ -723,6 +766,7 @@ namespace glz * @brief Direct lookup table for non-parameterized routes (optimization) */ std::unordered_map> direct_routes; + std::string last_route_error{}; /** * @brief Add a route to the radix tree @@ -731,25 +775,34 @@ namespace glz * @param path Path pattern for the route * @param handle Handler function for the route * @param constraints Optional parameter constraints - * @throws std::runtime_error if there's a route conflict + * @param error_message Optional output message for route registration failures + * @return true if the route was added, false if route registration failed */ - void add_route(http_method method, std::string_view path, handler handle, - const std::unordered_map& constraints = {}) + [[nodiscard]] bool add_route(http_method method, std::string_view path, handler handle, + const std::unordered_map& constraints = {}, + std::string* error_message = nullptr) { std::string path_str(path); + auto fail = [&](std::string message) -> bool { + if (error_message) { + *error_message = std::move(message); + } + return false; + }; + // Optimization: for non-parameterized routes, store them directly if (path_str.find(':') == std::string::npos && path_str.find('*') == std::string::npos) { // Check for conflicts first auto& method_handlers = direct_routes[path_str]; if (method_handlers.find(method) != method_handlers.end()) { - throw std::runtime_error("Route conflict: handler already exists for " + std::string(to_string(method)) + - " " + path_str); + return fail("Route conflict: handler already exists for " + std::string(to_string(method)) + " " + + path_str); } // Store the route directly method_handlers[method] = handle; - return; + return true; } // For parameterized routes, use the radix tree @@ -779,8 +832,8 @@ namespace glz } else if (current->parameter_child->parameter_name != param_name) { // Parameter name conflict - throw std::runtime_error("Route conflict: different parameter names at same position: :" + - current->parameter_child->parameter_name + " vs :" + param_name); + return fail("Route conflict: different parameter names at same position: :" + + current->parameter_child->parameter_name + " vs :" + param_name); } current = current->parameter_child.get(); @@ -791,7 +844,7 @@ namespace glz // Wildcards must be at the end of the route if (i != segments.size() - 1) { - throw std::runtime_error("Wildcard must be the last segment in route: " + path_str); + return fail("Wildcard must be the last segment in route: " + path_str); } if (!current->wildcard_child) { @@ -805,8 +858,8 @@ namespace glz } else if (current->wildcard_child->parameter_name != wildcard_name) { // Wildcard name conflict - throw std::runtime_error("Route conflict: different wildcard names at same position: *" + - current->wildcard_child->parameter_name + " vs *" + wildcard_name); + return fail("Route conflict: different wildcard names at same position: *" + + current->wildcard_child->parameter_name + " vs *" + wildcard_name); } current = current->wildcard_child.get(); @@ -828,8 +881,8 @@ namespace glz // Check for route conflict if (current->is_endpoint && current->handlers.find(method) != current->handlers.end()) { - throw std::runtime_error("Route conflict: handler already exists for " + std::string(to_string(method)) + - " " + path_str); + return fail("Route conflict: handler already exists for " + std::string(to_string(method)) + " " + + path_str); } // Mark as endpoint and set handler @@ -840,6 +893,8 @@ namespace glz if (!constraints.empty()) { current->constraints[method] = constraints; } + + return true; } /** diff --git a/include/glaze/rpc/registry.hpp b/include/glaze/rpc/registry.hpp index c84f23c617..3a4ed8ddbd 100644 --- a/include/glaze/rpc/registry.hpp +++ b/include/glaze/rpc/registry.hpp @@ -252,6 +252,50 @@ namespace glz register_members(value); } + // Non-throwing registration API for callers that need explicit route registration errors. + template + requires(glaze_object_t || reflectable) + [[nodiscard]] bool try_on(T& value, std::string* error_message = nullptr) + { + if (error_message) { + error_message->clear(); + } + + if constexpr (Proto == REST) { + endpoints.clear_route_error(); +#if __cpp_exceptions + try { + on(value); + } + catch (const std::exception& e) { + if (error_message) { + *error_message = e.what(); + } + return false; + } + catch (...) { + if (error_message) { + *error_message = "Unknown route registration error"; + } + return false; + } +#else + on(value); +#endif + if (endpoints.has_route_error()) { + if (error_message) { + *error_message = std::string(endpoints.route_error()); + } + return false; + } + } + else { + on(value); + } + + return true; + } + // Register multiple C++ types merged together, allowing the root endpoint to return a combined view template requires(sizeof...(Ts) > 0 && (... && (glaze_object_t || reflectable))) @@ -270,6 +314,50 @@ namespace glz }); } + // Non-throwing registration API for merged objects. + template + requires(sizeof...(Ts) > 0 && (... && (glaze_object_t || reflectable))) + [[nodiscard]] bool try_on(glz::merge& merged, std::string* error_message = nullptr) + { + if (error_message) { + error_message->clear(); + } + + if constexpr (Proto == REST) { + endpoints.clear_route_error(); +#if __cpp_exceptions + try { + on(merged); + } + catch (const std::exception& e) { + if (error_message) { + *error_message = e.what(); + } + return false; + } + catch (...) { + if (error_message) { + *error_message = "Unknown route registration error"; + } + return false; + } +#else + on(merged); +#endif + if (endpoints.has_route_error()) { + if (error_message) { + *error_message = std::string(endpoints.route_error()); + } + return false; + } + } + else { + on(merged); + } + + return true; + } + /// Message-based call for REPE protocol (deprecated) /// @deprecated Use the zero-copy span-based overload instead: /// `call(std::span, std::string&)` @@ -328,6 +416,7 @@ namespace glz resp.reset(req_view); repe::state_view state{req_view, resp}; +#if __cpp_exceptions try { it->second(state); } @@ -335,6 +424,9 @@ namespace glz resp.reset(req_view); resp.set_error(error_code::parse_error, detail::build_registry_error(in.query, e.what())); } +#else + it->second(state); +#endif } } else { @@ -412,6 +504,7 @@ namespace glz // Zero-copy call: state_view references the parsed request and response builder directly repe::state_view state{req, resp}; +#if __cpp_exceptions try { it->second(state); } @@ -425,6 +518,9 @@ namespace glz resp.set_error(error_code::parse_error, "Unknown error"); return; } +#else + it->second(state); +#endif // For notifications, response buffer stays empty (no response sent) } @@ -529,6 +625,7 @@ namespace glz bool has_params = !req.params.str.empty() && req.params.str != "null"; jsonrpc::state state{req.id, response, is_notification, has_params, req.params.str}; +#if __cpp_exceptions try { it->second(std::move(state)); } @@ -540,6 +637,9 @@ namespace glz return R"({"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error","data":)" + write_json(std::string_view{e.what()}).value_or("null") + R"(},"id":)" + id_json + "}"; } +#else + it->second(std::move(state)); +#endif if (is_notification) { return std::nullopt; diff --git a/tests/networking_tests/CMakeLists.txt b/tests/networking_tests/CMakeLists.txt index 8ffabfe461..824a6d7a31 100644 --- a/tests/networking_tests/CMakeLists.txt +++ b/tests/networking_tests/CMakeLists.txt @@ -1,6 +1,7 @@ # glz_asio is defined in the parent tests/CMakeLists.txt option(glaze_BUILD_SSL_TESTS "Build SSL/TLS tests (requires OpenSSL headers matching target architecture)" ON) +option(glaze_BUILD_REGISTRY_NOEXCEPT_TESTS "Build no-exceptions variants of registry networking tests" ON) # HTTP Examples add_subdirectory(http_examples) diff --git a/tests/networking_tests/jsonrpc_registry_test/CMakeLists.txt b/tests/networking_tests/jsonrpc_registry_test/CMakeLists.txt index 371a488a6e..d56c6d938f 100644 --- a/tests/networking_tests/jsonrpc_registry_test/CMakeLists.txt +++ b/tests/networking_tests/jsonrpc_registry_test/CMakeLists.txt @@ -5,3 +5,9 @@ add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +if(glaze_BUILD_REGISTRY_NOEXCEPT_TESTS) + add_executable(${PROJECT_NAME}_noexceptions ${PROJECT_NAME}.cpp) + target_link_libraries(${PROJECT_NAME}_noexceptions PRIVATE glz_test_common) + add_test(NAME ${PROJECT_NAME}_noexceptions COMMAND ${PROJECT_NAME}_noexceptions) +endif() diff --git a/tests/networking_tests/jsonrpc_registry_test/jsonrpc_registry_test.cpp b/tests/networking_tests/jsonrpc_registry_test/jsonrpc_registry_test.cpp index 300b6999be..4f98c20a20 100644 --- a/tests/networking_tests/jsonrpc_registry_test/jsonrpc_registry_test.cpp +++ b/tests/networking_tests/jsonrpc_registry_test/jsonrpc_registry_test.cpp @@ -374,6 +374,7 @@ suite jsonrpc_merge_tests = [] { }; }; +#if __cpp_exceptions struct throwing_functions_t { std::function throw_func = []() -> int { throw std::runtime_error("Test exception"); }; @@ -411,6 +412,7 @@ suite jsonrpc_exception_tests = [] { expect(!err) << "Response must be valid JSON: " << glz::format_error(err, response); }; }; +#endif suite jsonrpc_error_json_validity_tests = [] { "parse_error_with_special_chars_produces_valid_json"_test = [] { diff --git a/tests/networking_tests/registry_view_test/CMakeLists.txt b/tests/networking_tests/registry_view_test/CMakeLists.txt index 915702d784..9f1807730f 100644 --- a/tests/networking_tests/registry_view_test/CMakeLists.txt +++ b/tests/networking_tests/registry_view_test/CMakeLists.txt @@ -5,3 +5,9 @@ add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +if(glaze_BUILD_REGISTRY_NOEXCEPT_TESTS) + add_executable(${PROJECT_NAME}_noexceptions ${PROJECT_NAME}.cpp) + target_link_libraries(${PROJECT_NAME}_noexceptions PRIVATE glz_test_common) + add_test(NAME ${PROJECT_NAME}_noexceptions COMMAND ${PROJECT_NAME}_noexceptions) +endif() diff --git a/tests/networking_tests/repe_test/CMakeLists.txt b/tests/networking_tests/repe_test/CMakeLists.txt index c8e27cc2dd..443942bc56 100644 --- a/tests/networking_tests/repe_test/CMakeLists.txt +++ b/tests/networking_tests/repe_test/CMakeLists.txt @@ -7,3 +7,9 @@ add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +if(glaze_BUILD_REGISTRY_NOEXCEPT_TESTS) + add_executable(${PROJECT_NAME}_noexceptions ${PROJECT_NAME}.cpp) + target_link_libraries(${PROJECT_NAME}_noexceptions PRIVATE glz_test_common) + add_test(NAME ${PROJECT_NAME}_noexceptions COMMAND ${PROJECT_NAME}_noexceptions) +endif() diff --git a/tests/networking_tests/repe_test/repe_test.cpp b/tests/networking_tests/repe_test/repe_test.cpp index 62fd39d475..2c0f6c3aa6 100644 --- a/tests/networking_tests/repe_test/repe_test.cpp +++ b/tests/networking_tests/repe_test/repe_test.cpp @@ -712,11 +712,13 @@ suite validation_tests = [] { }; }; +#if __cpp_exceptions // Define throwing_functions_t at namespace scope for proper linkage struct throwing_functions_t { std::function throw_func = []() -> int { throw std::runtime_error("Test exception"); }; }; +#endif suite id_preservation_tests = [] { using namespace test_helpers; @@ -739,6 +741,7 @@ suite id_preservation_tests = [] { expect(response.body.find("invalid_query") != std::string::npos); }; +#if __cpp_exceptions "exception_error_preserves_id"_test = [] { glz::registry server{}; @@ -756,6 +759,7 @@ suite id_preservation_tests = [] { expect(response.header.id == 67890) << "ID should be preserved in exception error"; expect(response.body.find("Test exception") != std::string::npos); }; +#endif "header_validation_errors_preserve_id"_test = [] { glz::registry server{}; diff --git a/tests/networking_tests/rest_test/CMakeLists.txt b/tests/networking_tests/rest_test/CMakeLists.txt index 13669d15ba..cd646ca8bc 100644 --- a/tests/networking_tests/rest_test/CMakeLists.txt +++ b/tests/networking_tests/rest_test/CMakeLists.txt @@ -10,4 +10,10 @@ add_executable(rest_registry_test rest_registry_test.cpp) target_link_libraries(rest_registry_test PRIVATE glz_test_exceptions) -add_subdirectory(rest_server) \ No newline at end of file +if(glaze_BUILD_REGISTRY_NOEXCEPT_TESTS) + add_executable(rest_registry_test_noexceptions rest_registry_noexceptions_test.cpp) + target_link_libraries(rest_registry_test_noexceptions PRIVATE glz_test_common) + add_test(NAME rest_registry_test_noexceptions COMMAND rest_registry_test_noexceptions) +endif() + +add_subdirectory(rest_server) diff --git a/tests/networking_tests/rest_test/rest_registry_noexceptions_test.cpp b/tests/networking_tests/rest_test/rest_registry_noexceptions_test.cpp new file mode 100644 index 0000000000..ca1d60c38a --- /dev/null +++ b/tests/networking_tests/rest_test/rest_registry_noexceptions_test.cpp @@ -0,0 +1,86 @@ +// Glaze Library +// For the license information refer to glaze.hpp + +#include "glaze/rpc/registry.hpp" +#include "ut/ut.hpp" + +using namespace ut; + +struct rest_noexceptions_api_t +{ + int value{}; + + int get_value() const { return value; } + void set_value(const int v) { value = v; } + + struct glaze + { + using T = rest_noexceptions_api_t; + static constexpr auto value = glz::object("value", &T::value, "get_value", &T::get_value, "set_value", &T::set_value); + }; +}; + +suite rest_registry_noexceptions_tests = [] { + "rest_registry_registers_and_dispatches_without_exceptions"_test = [] { + glz::registry registry{}; + rest_noexceptions_api_t api{}; + registry.on(api); + + auto [put_handler, put_params] = registry.endpoints.match(glz::http_method::PUT, "/value"); + expect(put_handler != nullptr); + + glz::request put_req{ + .method = glz::http_method::PUT, .target = "/value", .path = "/value", .params = put_params, .body = "7"}; + glz::response put_res{}; + put_handler(put_req, put_res); + + expect(api.value == 7); + expect(put_res.status_code == 204); + + auto [get_handler, get_params] = registry.endpoints.match(glz::http_method::GET, "/value"); + expect(get_handler != nullptr); + + glz::request get_req{ + .method = glz::http_method::GET, .target = "/value", .path = "/value", .params = get_params}; + glz::response get_res{}; + get_handler(get_req, get_res); + + expect(get_res.response_body.find("7") != std::string::npos) << get_res.response_body; + }; + + "registry_try_on_reports_route_conflicts_without_exceptions"_test = [] { + glz::registry registry{}; + rest_noexceptions_api_t first{}; + rest_noexceptions_api_t second{}; + + std::string error{}; + auto ok = registry.try_on(first, &error); + expect(ok); + expect(error.empty()); + + ok = registry.try_on(second, &error); + expect(!ok); + expect(error.find("Route conflict") != std::string::npos) << error; + }; + + "route_registration_errors_are_observable_without_exceptions"_test = [] { + glz::http_router router{}; + + std::string first_error{}; + auto ok = router.try_route(glz::http_method::GET, "/dup", [](const glz::request&, glz::response&) {}, {}, + &first_error); + expect(ok); + expect(first_error.empty()); + + std::string second_error{}; + ok = router.try_route(glz::http_method::GET, "/dup", [](const glz::request&, glz::response&) {}, {}, + &second_error); + expect(!ok); + expect(!second_error.empty()); + expect(second_error.find("Route conflict") != std::string::npos) << second_error; + expect(router.has_route_error()); + expect(router.route_error().find("Route conflict") != std::string_view::npos); + }; +}; + +int main() { return 0; }