From 43cf1822c6b2521bf863afdf7f9f48acf18a2628 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 20 Feb 2026 16:22:01 -0500 Subject: [PATCH] Resolve #2369 --- .gitignore | 1 + README.md | 28 +++++++++++ httplib.h | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/test.cc | 107 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) diff --git a/.gitignore b/.gitignore index 01cc6cf..1ea2142 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ test/test_no_tls test/server_fuzzer test/test_proxy test/test_proxy_mbedtls +test/test_proxy_wolfssl test/test_split test/test_split_mbedtls test/test_split_wolfssl diff --git a/README.md b/README.md index f40893c..80d6fc4 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,34 @@ svr.set_pre_request_handler([](const auto& req, auto& res) { }); ``` +### Response user data + +`res.user_data` is a `std::map` that lets pre-routing or pre-request handlers pass arbitrary data to route handlers. + +```cpp +struct AuthContext { + std::string user_id; + std::string role; +}; + +svr.set_pre_routing_handler([](const auto& req, auto& res) { + auto token = req.get_header_value("Authorization"); + res.user_data["auth"] = AuthContext{decode_token(token)}; + return Server::HandlerResponse::Unhandled; +}); + +svr.Get("/me", [](const auto& /*req*/, auto& res) { + auto* ctx = httplib::any_cast(&res.user_data["auth"]); + if (!ctx) { + res.status = StatusCode::Unauthorized_401; + return; + } + res.set_content("Hello " + ctx->user_id, "text/plain"); +}); +``` + +`httplib::any` mirrors the C++17 `std::any` API. On C++17 and later it is an alias for `std::any`; on C++11/14 a compatible implementation is provided. + ### Form data handling #### URL-encoded form data ('application/x-www-form-urlencoded') diff --git a/httplib.h b/httplib.h index 32892cb..8de2719 100644 --- a/httplib.h +++ b/httplib.h @@ -352,6 +352,9 @@ using socket_t = int; #include #include #include +#if __cplusplus >= 201703L +#include +#endif #if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \ defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) @@ -775,6 +778,135 @@ using Match = std::smatch; using DownloadProgress = std::function; using UploadProgress = std::function; +// ---------------------------------------------------------------------------- +// httplib::any — type-erased value container (C++11 compatible) +// On C++17+ builds, thin wrappers around std::any are provided. +// ---------------------------------------------------------------------------- + +#if __cplusplus >= 201703L + +using any = std::any; +using bad_any_cast = std::bad_any_cast; + +template T any_cast(const any &a) { return std::any_cast(a); } +template T any_cast(any &a) { return std::any_cast(a); } +template T any_cast(any &&a) { + return std::any_cast(std::move(a)); +} +template const T *any_cast(const any *a) noexcept { + return std::any_cast(a); +} +template T *any_cast(any *a) noexcept { + return std::any_cast(a); +} + +#else // C++11/14 implementation + +class bad_any_cast : public std::bad_cast { +public: + const char *what() const noexcept override { return "bad any_cast"; } +}; + +namespace detail { + +using any_type_id = const void *; + +// Returns a unique per-type ID without RTTI. +// The static address is stable across TUs because function templates are +// implicitly inline and the ODR merges their statics into one. +template any_type_id any_typeid() noexcept { + static const char id = 0; + return &id; +} + +struct any_storage { + virtual ~any_storage() = default; + virtual std::unique_ptr clone() const = 0; + virtual any_type_id type_id() const noexcept = 0; +}; + +template struct any_value final : any_storage { + T value; + template explicit any_value(U &&v) : value(std::forward(v)) {} + std::unique_ptr clone() const override { + return std::unique_ptr(new any_value(value)); + } + any_type_id type_id() const noexcept override { return any_typeid(); } +}; + +} // namespace detail + +class any { + std::unique_ptr storage_; + +public: + any() noexcept = default; + any(const any &o) : storage_(o.storage_ ? o.storage_->clone() : nullptr) {} + any(any &&) noexcept = default; + any &operator=(const any &o) { + storage_ = o.storage_ ? o.storage_->clone() : nullptr; + return *this; + } + any &operator=(any &&) noexcept = default; + + template < + typename T, typename D = typename std::decay::type, + typename std::enable_if::value, int>::type = 0> + any(T &&v) : storage_(new detail::any_value(std::forward(v))) {} + + template < + typename T, typename D = typename std::decay::type, + typename std::enable_if::value, int>::type = 0> + any &operator=(T &&v) { + storage_.reset(new detail::any_value(std::forward(v))); + return *this; + } + + bool has_value() const noexcept { return storage_ != nullptr; } + void reset() noexcept { storage_.reset(); } + + template friend T *any_cast(any *a) noexcept; + template friend const T *any_cast(const any *a) noexcept; +}; + +template T *any_cast(any *a) noexcept { + if (!a || !a->storage_) { return nullptr; } + if (a->storage_->type_id() != detail::any_typeid()) { return nullptr; } + return &static_cast *>(a->storage_.get())->value; +} + +template const T *any_cast(const any *a) noexcept { + if (!a || !a->storage_) { return nullptr; } + if (a->storage_->type_id() != detail::any_typeid()) { return nullptr; } + return &static_cast *>(a->storage_.get())->value; +} + +template T any_cast(const any &a) { + using U = + typename std::remove_cv::type>::type; + const U *p = any_cast(&a); + if (!p) { throw bad_any_cast{}; } + return static_cast(*p); +} + +template T any_cast(any &a) { + using U = + typename std::remove_cv::type>::type; + U *p = any_cast(&a); + if (!p) { throw bad_any_cast{}; } + return static_cast(*p); +} + +template T any_cast(any &&a) { + using U = + typename std::remove_cv::type>::type; + U *p = any_cast(&a); + if (!p) { throw bad_any_cast{}; } + return static_cast(std::move(*p)); +} + +#endif // __cplusplus >= 201703L + struct Response; using ResponseHandler = std::function; @@ -1074,6 +1206,10 @@ struct Response { std::string body; std::string location; // Redirect location + // User-defined context — set by pre-routing/pre-request handlers and read + // by route handlers to pass arbitrary data (e.g. decoded auth tokens). + std::map user_data; + bool has_header(const std::string &key) const; std::string get_header_value(const std::string &key, const char *def = "", size_t id = 0) const; diff --git a/test/test.cc b/test/test.cc index 257b78e..3ae89a0 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3000,6 +3000,113 @@ TEST(RequestHandlerTest, PreRequestHandler) { } } +TEST(AnyTest, BasicOperations) { + // Default construction + httplib::any a; + EXPECT_FALSE(a.has_value()); + + // Value construction and any_cast (pointer form, noexcept) + httplib::any b(42); + EXPECT_TRUE(b.has_value()); + auto *p = httplib::any_cast(&b); + ASSERT_NE(nullptr, p); + EXPECT_EQ(42, *p); + + // Type mismatch → nullptr + auto *q = httplib::any_cast(&b); + EXPECT_EQ(nullptr, q); + + // any_cast (value form) succeeds + EXPECT_EQ(42, httplib::any_cast(b)); + + // any_cast (value form) throws on type mismatch + EXPECT_THROW(httplib::any_cast(b), httplib::bad_any_cast); + + // Copy + httplib::any c = b; + EXPECT_EQ(42, httplib::any_cast(c)); + + // Move + httplib::any d = std::move(c); + EXPECT_EQ(42, httplib::any_cast(d)); + + // Assignment with different type + b = std::string("hello"); + EXPECT_EQ("hello", httplib::any_cast(b)); + + // Reset + b.reset(); + EXPECT_FALSE(b.has_value()); +} + +TEST(RequestHandlerTest, ResponseUserDataInPreRouting) { + struct AuthCtx { + std::string user_id; + }; + + Server svr; + + svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) { + res.user_data["auth"] = AuthCtx{"alice"}; + return Server::HandlerResponse::Unhandled; + }); + + svr.Get("/me", [](const Request & /*req*/, Response &res) { + auto *ctx = httplib::any_cast(&res.user_data["auth"]); + ASSERT_NE(nullptr, ctx); + res.set_content("Hello " + ctx->user_id, "text/plain"); + }); + + auto thread = std::thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + auto res = cli.Get("/me"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("Hello alice", res->body); +} + +TEST(RequestHandlerTest, ResponseUserDataInPreRequest) { + struct RoleCtx { + std::string role; + }; + + Server svr; + + svr.set_pre_request_handler([](const Request & /*req*/, Response &res) { + res.user_data["role"] = RoleCtx{"admin"}; + return Server::HandlerResponse::Unhandled; + }); + + svr.Get("/role", [](const Request & /*req*/, Response &res) { + auto *ctx = httplib::any_cast(&res.user_data["role"]); + ASSERT_NE(nullptr, ctx); + res.set_content(ctx->role, "text/plain"); + }); + + auto thread = std::thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + auto res = cli.Get("/role"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("admin", res->body); +} + TEST(InvalidFormatTest, StatusCode) { Server svr;