Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 82 additions & 27 deletions include/glaze/net/http_router.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <memory>
#include <optional>
#include <source_location>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
Expand Down Expand Up @@ -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<int>(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
*/
Expand Down Expand Up @@ -723,6 +766,7 @@ namespace glz
* @brief Direct lookup table for non-parameterized routes (optimization)
*/
std::unordered_map<std::string, std::unordered_map<http_method, handler>> direct_routes;
std::string last_route_error{};

/**
* @brief Add a route to the radix tree
Expand All @@ -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<std::string, param_constraint>& constraints = {})
[[nodiscard]] bool add_route(http_method method, std::string_view path, handler handle,
const std::unordered_map<std::string, param_constraint>& 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
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -840,6 +893,8 @@ namespace glz
if (!constraints.empty()) {
current->constraints[method] = constraints;
}

return true;
}

/**
Expand Down
100 changes: 100 additions & 0 deletions include/glaze/rpc/registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,50 @@ namespace glz
register_members<root, T, parent>(value);
}

// Non-throwing registration API for callers that need explicit route registration errors.
template <const std::string_view& root = detail::empty_path, class T, const std::string_view& parent = root>
requires(glaze_object_t<T> || reflectable<T>)
[[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<root, T, parent>(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<root, T, parent>(value);
#endif
if (endpoints.has_route_error()) {
if (error_message) {
*error_message = std::string(endpoints.route_error());
}
return false;
}
}
else {
on<root, T, parent>(value);
}

return true;
}

// Register multiple C++ types merged together, allowing the root endpoint to return a combined view
template <const std::string_view& root = detail::empty_path, class... Ts>
requires(sizeof...(Ts) > 0 && (... && (glaze_object_t<Ts> || reflectable<Ts>)))
Expand All @@ -270,6 +314,50 @@ namespace glz
});
}

// Non-throwing registration API for merged objects.
template <const std::string_view& root = detail::empty_path, class... Ts>
requires(sizeof...(Ts) > 0 && (... && (glaze_object_t<Ts> || reflectable<Ts>)))
[[nodiscard]] bool try_on(glz::merge<Ts...>& 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<root>(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<root>(merged);
#endif
if (endpoints.has_route_error()) {
if (error_message) {
*error_message = std::string(endpoints.route_error());
}
return false;
}
}
else {
on<root>(merged);
}

return true;
}

/// Message-based call for REPE protocol (deprecated)
/// @deprecated Use the zero-copy span-based overload instead:
/// `call(std::span<const char>, std::string&)`
Expand Down Expand Up @@ -328,13 +416,17 @@ namespace glz
resp.reset(req_view);
repe::state_view state{req_view, resp};

#if __cpp_exceptions
try {
it->second(state);
}
catch (const std::exception& e) {
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 {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tests/networking_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 6 additions & 0 deletions tests/networking_tests/jsonrpc_registry_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ suite jsonrpc_merge_tests = [] {
};
};

#if __cpp_exceptions
struct throwing_functions_t
{
std::function<int()> throw_func = []() -> int { throw std::runtime_error("Test exception"); };
Expand Down Expand Up @@ -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 = [] {
Expand Down
6 changes: 6 additions & 0 deletions tests/networking_tests/registry_view_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading
Loading