diff --git a/include/boost/http_proto/detail/type_traits.hpp b/include/boost/http_proto/detail/type_traits.hpp index f577619c..590b2974 100644 --- a/include/boost/http_proto/detail/type_traits.hpp +++ b/include/boost/http_proto/detail/type_traits.hpp @@ -17,6 +17,9 @@ namespace boost { namespace http_proto { namespace detail { +template struct make_void { typedef void type; }; +template using void_t = typename make_void::type; + template struct remove_cvref { diff --git a/include/boost/http_proto/error.hpp b/include/boost/http_proto/error.hpp index 9a1cbb12..9927521e 100644 --- a/include/boost/http_proto/error.hpp +++ b/include/boost/http_proto/error.hpp @@ -197,7 +197,11 @@ enum class error /** A dynamic buffer's maximum size would be exceeded. */ - buffer_overflow + buffer_overflow, + + /** An unhandled exception occurred while routing a request + */ + unhandled_exception }; // VFALCO we need a bad_message condition? diff --git a/include/boost/http_proto/server/basic_router.hpp b/include/boost/http_proto/server/basic_router.hpp index f79fdc4b..03bcc773 100644 --- a/include/boost/http_proto/server/basic_router.hpp +++ b/include/boost/http_proto/server/basic_router.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -354,6 +355,37 @@ class basic_router : public /*detail::*/any_router std::false_type >::type {}; + template + struct except_type : std::false_type {}; + + template + struct except_type{} && ( + capy::detail::type_list_size::arg_types>{} == 3) && + std::is_convertible::arg_types>::type>::value && + std::is_convertible::arg_types>::type>{} + >::type> + : std::true_type + { + using type = typename std::decay::arg_types>::type>::type; + }; + + template + struct except_types; + + template + struct except_types : std::true_type {}; + + template + struct except_types : std::integral_constant{} && except_types{}> + {}; + public: /** The type of request object used in handlers */ @@ -524,6 +556,32 @@ class basic_router : public /*detail::*/any_router std::forward

(h1), std::forward(hn)...); } + /** Add a global exception handler. + */ + template + void except( + core::string_view pattern, + H1&& h1, HN... hn) + { + // If you get a compile error on this line it means that one or + // more of the provided types is not a valid exception handler + BOOST_CORE_STATIC_ASSERT(except_types<0, H1, HN...>::value); + add_impl(pattern, make_except_list( + std::forward

(h1), std::forward(hn)...)); + } + + template::value>::type> + void except(H1&& h1, HN&&... hn) + { + // If you get a compile error on this line it means that one or + // more of the provided types is not a valid exception handler + BOOST_CORE_STATIC_ASSERT(except_types<0, H1, HN...>::value); + except(core::string_view(), + std::forward

(h1), std::forward(hn)...); + } + /** Add handlers for all HTTP methods matching a path pattern. This registers regular handlers for the specified path pattern, @@ -736,8 +794,31 @@ class basic_router : public /*detail::*/any_router } private: + struct undo_resume + { + std::size_t& resume; + bool undo_ = true; + ~undo_resume() + { + if(undo_) + resume = 0; + } + undo_resume( + std::size_t& resume_) noexcept + : resume(resume_) + { + } + void cancel() noexcept + { + undo_ = false; + } + }; + // wrapper for route handlers - template + template< + class H, + class Ty = handler_type::type > > struct handler_impl : any_handler { typename std::decay::type h; @@ -751,8 +832,7 @@ class basic_router : public /*detail::*/any_router std::size_t count() const noexcept override { - return count( - handler_type{}); + return count(Ty{}); } route_result @@ -762,8 +842,7 @@ class basic_router : public /*detail::*/any_router { return invoke( static_cast(req), - static_cast(res), - handler_type{}); + static_cast(res), Ty{}); } private: @@ -795,16 +874,20 @@ class basic_router : public /*detail::*/any_router route_result invoke(Req& req, Res& res, std::integral_constant) const { - auto const& ec = static_cast< - basic_response const&>(res).ec_; - if(ec.failed()) + auto& res_ = static_cast< + basic_response&>(res); + if( res_.ec_.failed() || + res_.ep_) return http_proto::route::next; - // avoid racing on res.resume_ - res.resume_ = res.pos_; + // avoid racing on res_.resume_ + undo_resume u(res_.resume_); + res_.resume_ = res_.pos_; auto rv = h(req, res); if(rv == http_proto::route::detach) + { + u.cancel(); return rv; - res.resume_ = 0; // revert + } return rv; } @@ -813,16 +896,19 @@ class basic_router : public /*detail::*/any_router invoke(Req& req, Res& res, std::integral_constant) const { - auto const& ec = static_cast< - basic_response const&>(res).ec_; - if(! ec.failed()) + auto& res_ = static_cast< + basic_response&>(res); + if(! res_.ec_.failed()) return http_proto::route::next; // avoid racing on res.resume_ - res.resume_ = res.pos_; - auto rv = h(req, res, ec); + res_.resume_ = res_.pos_; + undo_resume u(res_.resume_); + auto rv = h(req, res, res_.ec_); if(rv == http_proto::route::detach) + { + u.cancel(); return rv; - res.resume_ = 0; // revert + } return rv; } @@ -830,15 +916,76 @@ class basic_router : public /*detail::*/any_router route_result invoke(Req& req, Res& res, std::integral_constant) const { - auto const& ec = static_cast< - basic_response const&>(res).ec_; - if( res.resume_ > 0 || - ! ec.failed()) + auto const& res_ = static_cast< + basic_response const&>(res); + if( res_.resume_ > 0 || + ( ! res_.ec_.failed() && + ! res_.ep_)) return h.dispatch_impl(req, res); return http_proto::route::next; } }; + template::type>::type> + struct except_impl : any_handler + { + typename std::decay::type h; + + template + explicit except_impl(Args&&... args) + : h(std::forward(args)...) + { + } + + std::size_t + count() const noexcept override + { + return 1; + } + + route_result + invoke(Req& req, Res& res) const override + { + #ifndef BOOST_NO_EXCEPTIONS + auto& res_ = static_cast< + basic_response&>(res); + if( ! res_.ec_.failed() && + ! res_.ep_) + return http_proto::route::next; + volatile int dummy = sizeof(E); + (void)(dummy); + try + { + std::rethrow_exception(res_.ep_); + } + catch(E const& ex) + { + // avoid racing on res.resume_ + res_.resume_ = res_.pos_; + undo_resume u(res_.resume_); + // VFALCO What if h throws? + auto rv = h(req, res, ex); + if(rv == http_proto::route::detach) + { + u.cancel(); + return rv; + } + return rv; + } + catch(...) + { + res_.ep_ = std::current_exception(); + return http_proto::route::next; + } + #else + (void)req; + (void)res; + return http_proto::route::next; + #endif + } + }; + template struct handler_list_impl : handler_list { @@ -850,6 +997,15 @@ class basic_router : public /*detail::*/any_router assign<0>(std::forward(hn)...); } + // exception handlers + template + explicit handler_list_impl(int, HN&&... hn) + { + n = sizeof...(HN); + p = v; + assign<0>(0, std::forward(hn)...); + } + private: template void assign(H1&& h1, HN&&... hn) @@ -859,8 +1015,17 @@ class basic_router : public /*detail::*/any_router assign(std::forward(hn)...); } + // exception handlers + template + void assign(int, H1&& h1, HN&&... hn) + { + v[I] = handler_ptr(new except_impl

( + std::forward

(h1))); + assign(0, std::forward(hn)...); + } + template - void assign() + void assign(int = 0) { } @@ -876,6 +1041,15 @@ class basic_router : public /*detail::*/any_router std::forward(hn)...); } + template + static auto + make_except_list(HN&&... hn) -> + handler_list_impl + { + return handler_list_impl( + 0, std::forward(hn)...); + } + void append(layer& e, http_proto::method verb, handler_list const& handlers) diff --git a/include/boost/http_proto/server/cors.hpp b/include/boost/http_proto/server/cors.hpp new file mode 100644 index 00000000..eb6dd288 --- /dev/null +++ b/include/boost/http_proto/server/cors.hpp @@ -0,0 +1,53 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +#ifndef BOOST_HTTP_PROTO_SERVER_CORS_HPP +#define BOOST_HTTP_PROTO_SERVER_CORS_HPP + +#include +#include +#include +#include + +namespace boost { +namespace http_proto { + +struct cors_options +{ + std::string origin; + std::string methods; + std::string allowedHeaders; + std::string exposedHeaders; + std::chrono::seconds max_age{ 0 }; + status result = status::no_content; + bool preFligthContinue = false; + bool credentials = false; +}; + +class cors +{ +public: + BOOST_HTTP_PROTO_DECL + explicit cors( + cors_options options = {}) noexcept; + + BOOST_HTTP_PROTO_DECL + route_result + operator()( + Request& req, + Response& res) const; + +private: + cors_options options_; +}; + +} // http_proto +} // boost + +#endif diff --git a/include/boost/http_proto/server/router_types.hpp b/include/boost/http_proto/server/router_types.hpp index be7f281d..f833aa94 100644 --- a/include/boost/http_proto/server/router_types.hpp +++ b/include/boost/http_proto/server/router_types.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -340,7 +341,7 @@ class basic_response std::size_t pos_ = 0; std::size_t resume_ = 0; system::error_code ec_; - unsigned int opt_ = 0; + std::exception_ptr ep_; }; } // http_proto diff --git a/src/error.cpp b/src/error.cpp index c5454f66..6324264a 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -21,7 +21,7 @@ const char* error_cat_type:: name() const noexcept { - return "boost.http.proto"; + return "boost.http"; } std::string @@ -75,6 +75,7 @@ message( case error::numeric_overflow: return "numeric overflow"; case error::multiple_content_length: return "multiple Content-Length"; case error::buffer_overflow: return "buffer overflow"; + case error::unhandled_exception: return "unhandled exception"; default: return "unknown"; } @@ -86,7 +87,7 @@ const char* condition_cat_type:: name() const noexcept { - return "boost.http.proto"; + return "boost.http"; } std::string diff --git a/src/server/basic_router.cpp b/src/server/basic_router.cpp index 1cb3c264..ef3956a5 100644 --- a/src/server/basic_router.cpp +++ b/src/server/basic_router.cpp @@ -661,7 +661,17 @@ dispatch_impl( // we cannot do anything after do_dispatch returns, // other than return the route_result, or else we // could race with the detached operation trying to resume. - return do_dispatch(req, res); + auto rv = do_dispatch(req, res); + if(rv == route::detach) + return rv; + if(res.ep_) + { + res.ep_ = nullptr; + return error::unhandled_exception; + } + if( res.ec_.failed()) + res.ec_ = {}; + return rv; } // recursive dispatch @@ -715,9 +725,6 @@ dispatch_impl( else if((impl_->opt & 16) != 0) req.strict = false; - // nested routers count as 1 call - //++res.pos_; - match_result mr; for(auto const& i : impl_->layers) { @@ -779,7 +786,24 @@ dispatch_impl( if(res.pos_ != res.resume_) { // call the handler + #ifdef BOOST_NO_EXCEPTIONS rv = e.handler->invoke(req, res); + #else + try + { + rv = e.handler->invoke(req, res); + if(res.ec_.failed()) + res.ep_ = {}; // transition to error mode + } + catch(...) + { + if(res.ec_.failed()) + res.ec_ = {}; // transition to except mode + res.ep_ = std::current_exception(); + rv = route::next; + } + #endif + // res.pos_ can be incremented further // inside the above call to invoke. if(rv == route::detach) @@ -807,7 +831,13 @@ dispatch_impl( if( rv == route::send || rv == route::complete || rv == route::close) + { + if( res.ec_.failed()) + res.ec_ = {}; + if( res.ep_) + res.ep_ = nullptr; return rv; + } if(rv == route::next) continue; // next entry if(rv == route::next_route) diff --git a/src/server/cors.cpp b/src/server/cors.cpp new file mode 100644 index 00000000..4293f6a0 --- /dev/null +++ b/src/server/cors.cpp @@ -0,0 +1,192 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +#include +#include + +namespace boost { +namespace http_proto { + +cors:: +cors( + cors_options options) noexcept + : options_(std::move(options)) +{ + // VFALCO TODO Validate the strings in options against RFC +} + +namespace { + +struct Vary +{ + Vary(Response& res) + : res_(res) + { + } + + void set(field f, core::string_view s) + { + res_.message.set(f, s); + } + + void append(field f, core::string_view v) + { + auto it = res_.message.find(f); + if (it != res_.message.end()) + { + std::string s = it->value; + s += ", "; + s += v; + res_.message.set(it, s); + } + else + { + res_.message.set(f, v); + } + } + +private: + Response& res_; + std::string v_; +}; + +} // (anon) + +// Access-Control-Allow-Origin +static void setOrigin( + Vary& v, + Request const&, + cors_options const& options) +{ + if( options.origin.empty() || + options.origin == "*") + { + v.set(field::access_control_allow_origin, "*"); + return; + } + + v.set( + field::access_control_allow_origin, + options.origin); + v.append(field::vary, to_string(field::origin)); +} + +// Access-Control-Allow-Methods +static void setMethods( + Vary& v, + cors_options const& options) +{ + if(! options.methods.empty()) + { + v.set( + field::access_control_allow_methods, + options.methods); + return; + } + v.set( + field::access_control_allow_methods, + "GET,HEAD,PUT,PATCH,POST,DELETE"); +} + +// Access-Control-Allow-Credentials +static void setCredentials( + Vary& v, + cors_options const& options) +{ + if(! options.credentials) + return; + v.set( + field::access_control_allow_credentials, + "true"); +} + +// Access-Control-Allowed-Headers +static void setAllowedHeaders( + Vary& v, + Request const& req, + cors_options const& options) +{ + if(! options.allowedHeaders.empty()) + { + v.set( + field::access_control_allow_headers, + options.allowedHeaders); + return; + } + auto s = req.message.value_or( + field::access_control_request_headers, ""); + if(! s.empty()) + { + v.set( + field::access_control_allow_headers, + s); + v.append(field::vary, s); + } +} + +// Access-Control-Expose-Headers +static void setExposeHeaders( + Vary& v, + cors_options const& options) +{ + if(options.exposedHeaders.empty()) + return; + v.set( + field::access_control_expose_headers, + options.exposedHeaders); +} + +// Access-Control-Max-Age +static void setMaxAge( + Vary& v, + cors_options const& options) +{ + if(options.max_age.count() == 0) + return; + v.set( + field::access_control_max_age, + std::to_string( + options.max_age.count())); +} + +route_result +cors:: +operator()( + Request& req, + Response& res) const +{ + Vary v(res); + if(req.message.method() == + method::options) + { + // preflight + setOrigin(v, req, options_); + setMethods(v, options_); + setCredentials(v, options_); + setAllowedHeaders(v, req, options_); + setMaxAge(v, options_); + setExposeHeaders(v, options_); + + if(options_.preFligthContinue) + return route::next; + // Safari and others need this for 204 or may hang + res.message.set_status(options_.result); + res.message.set_content_length(0); + res.serializer.start(res.message); + return route::send; + } + // actual response + setOrigin(v, req, options_); + setCredentials(v, options_); + setExposeHeaders(v, options_); + return route::next; +} + +} // http_proto +} // boost diff --git a/test/unit/error.cpp b/test/unit/error.cpp index 6fe3c2cb..f7f6d10c 100644 --- a/test/unit/error.cpp +++ b/test/unit/error.cpp @@ -68,7 +68,7 @@ class error_test void run() { - char const* const n = "boost.http.proto"; + char const* const n = "boost.http"; check(n, error::expect_100_continue); check(n, error::end_of_message); diff --git a/test/unit/server/basic_router.cpp b/test/unit/server/basic_router.cpp index 6d40f0df..a476ea1c 100644 --- a/test/unit/server/basic_router.cpp +++ b/test/unit/server/basic_router.cpp @@ -133,6 +133,37 @@ struct basic_router_test system::error_code ec_; }; + // A handler which throws + template + struct throw_ex + { + ~throw_ex() + { + if(alive_) + BOOST_TEST(called_); + } + + throw_ex() = default; + + throw_ex(throw_ex&& other) + { + BOOST_ASSERT(other.alive_); + BOOST_ASSERT(! other.called_); + alive_ = true; + other.alive_ = false; + } + + route_result operator()(Req&, Res&) const + { + called_ = true; + throw E("ex"); + } + + private: + bool alive_ = true; + bool mutable called_ = false; + }; + /** An error handler for testing */ struct err_handler @@ -192,6 +223,56 @@ struct basic_router_test system::error_code ec_; }; + /** An exception handler for testing + */ + template + struct ex_handler + { + ~ex_handler() + { + if(alive_) + BOOST_TEST_EQ(called_, want_ != 0); + } + + ex_handler(ex_handler const&) = delete; + + explicit ex_handler( + int want) + : want_(want) + { + } + + ex_handler(ex_handler&& other) + { + BOOST_ASSERT(other.alive_); + BOOST_ASSERT(! other.called_); + want_ = other.want_; + alive_ = true; + other.alive_ = false; + } + + route_result operator()( + Req&, Res&, E const&) const + { + called_ = true; + switch(want_) + { + default: + case 0: return route::close; + case 1: return route::send; + case 2: return route::next; + } + } + + private: + // 0 = not called + // 1 = send + // 2 = next + int want_; + bool alive_ = true; + bool mutable called_ = false; + }; + // handler to check base_url and path struct path { @@ -290,6 +371,30 @@ struct basic_router_test return err_handler(3, ec); } + struct none + { + }; + + // must NOT be called + static ex_handler ex_skip() + { + return ex_handler(0); + } + + // must be called, returns route::send + template + static ex_handler ex_send() + { + return ex_handler(1); + } + + // must be called, returns route::next + template + static ex_handler ex_next() + { + return ex_handler(2); + } + using test_router = basic_router; void check( @@ -1278,6 +1383,59 @@ struct basic_router_test } } + void testExcept() + { +#ifndef BOOST_NO_EXCEPTIONS + { + test_router r; + r.except(ex_skip()); + check(r, "/", route::next); + } + { + test_router r; + r.use(throw_ex()); + check(r, "/", error::unhandled_exception); + } + { + test_router r; + r.except(ex_skip()); + r.use(throw_ex()); + check(r, "/", error::unhandled_exception); + } + { + test_router r; + r.except(ex_skip()); + r.use(throw_ex()); + r.except(ex_send()); + check(r, "/"); + } + { + test_router r; + r.except(ex_skip()); + r.use(throw_ex()); + r.except( + ex_skip(), + ex_next()); + check(r, "/", error::unhandled_exception); + } + { + test_router r; + r.except(ex_skip()); + r.use(throw_ex()); + r.except(ex_skip()); + check(r, "/", error::unhandled_exception); + } + { + test_router r; + r.except(ex_skip()); + r.use(throw_ex()); + r.except(ex_skip()); + r.except(ex_send()); + check(r, "/"); + } +#endif + } + void testPath() { auto const path = []( @@ -1499,6 +1657,7 @@ struct basic_router_test testRoute(); testSubRouter(); testErr(); + testExcept(); testPath(); testPctDecode(); testDetach(); diff --git a/test/unit/server/cors.cpp b/test/unit/server/cors.cpp new file mode 100644 index 00000000..a49cc9cc --- /dev/null +++ b/test/unit/server/cors.cpp @@ -0,0 +1,108 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +// Test that header file is self-contained. +#include +#include "src/rfc/detail/rules.hpp" + +#include "test_suite.hpp" + +namespace boost { +namespace http_proto { + +#if 0 +class field_item +{ +public: + field_item( + core::string_view s) + : s_(s) + { + grammar::parse(s_, + detail::field_name_rule).value(); + } + + field_item( + field f) noexcept + : s_(to_string(f)) + { + } + + operator core::string_view() const noexcept + { + return s_; + } + +private: + core::string_view s_; +}; + +template +struct list +{ + struct item + { + core::string_view s; + + template< + class T, + class = typename std::enable_if< + std::is_constructible< + Element, T>::value>::type> + item(T&& t) + : s(Element(std::forward(t))) + { + } + }; + +public: + list(std::initializer_list init) + { + if(init.size() == 0) + return; + auto it = init.begin(); + s_ = it->s; + while(++it != init.end()) + { + s_.push_back(','); + s_.append(it->s.data(), + it->s.size()); + } + } + + core::string_view get() const noexcept + { + return s_; + } + +private: + std::string s_; +}; +#endif + +struct cors_test +{ + void run() + { +#if 0 + list v({ + field::access_control_allow_origin, + "example.com", + "example.org" + }); +#endif + } +}; + +TEST_SUITE( + cors_test, + "boost.http_proto.server.cors"); + +} // http_proto +} // boost