From d1dc4fdad31bfd67233b3ef905e8e70c5918475e Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 14:28:59 +0200 Subject: [PATCH 1/2] build: bump cpp-httplib to 0.47.0 0.16.3 was dropped from conan-center-index - only 0.28.0 and 0.47.0 are left - so the pin had stopped receiving fixes entirely. The bump was blocked until conan-io/conan-center-index#30607 landed. Every version from 0.28.0 on compiles the CFHost-based asynchronous resolver behind `CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO`, which 0.16.3 does not have at all, and the recipe linked CoreFoundation/CFNetwork only in the narrow macOS keychain-certs case - never for iOS. Since `Server::create_server_socket` goes through `detail::create_socket` -> `getaddrinfo_with_timeout`, the server-only build pulls that resolver in, so the xcframework would have failed to link on the device slice. The fixed recipe links the frameworks on every Apple OS whenever the resolver is compiled in, and the iOS binary now carries both. Measured on the http server suite, three runs each: serving a rendered view takes 5.03 s on 0.16.3 and 0.03 s on 0.47.0. The server-side read and keep-alive timeout constants are identical between the two, so this is an upstream behaviour fix rather than a tunable. `use_non_blocking_getaddrinfo` stays at its default of True: it is what gives the bind-time resolution a timeout at all, and turning it off would trade two Apple frameworks for an unbounded blocking `getaddrinfo`. The lockfile is edited on the one line rather than regenerated - a full `scripts/conan_lock` run also re-pins pugixml and drops the option-gated entries, which does not belong in a version bump. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YSePnR4Jcj5cq3H1D11xcd --- conan.lock | 2 +- conanfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conan.lock b/conan.lock index ff0ff718..9447dfe6 100644 --- a/conan.lock +++ b/conan.lock @@ -13,7 +13,7 @@ "miniz/3.0.2#bfbce07c6654293cce27ee24129d2df7%1743673472.805", "gtest/1.14.0#f8f0757a574a8dd747d16af62d6eb1b7%1743410807.169", "cryptopp/8.9.0#7a51e0038756b21bc3a6b82d681d5906%1758206597.119", - "cpp-httplib/0.16.3#7aa89fbb81ffd19539a49fc132502966%1748426320.106", + "cpp-httplib/0.47.0#add6673ff352c26898ed2650453e706e%1784539639.401", "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465", "argon2/20190702-odr#965901884bc82ec8a7c0a1305d42c127%1784987057.981858" ], diff --git a/conanfile.py b/conanfile.py index 100abcc4..67687c06 100644 --- a/conanfile.py +++ b/conanfile.py @@ -54,7 +54,7 @@ def requirements(self): self.requires("uchardet/0.0.8") self.requires("utfcpp/4.0.9") if self.options.get_safe("with_http_server", False): - self.requires("cpp-httplib/0.16.3") + self.requires("cpp-httplib/0.47.0") self.requires("argon2/20190702-odr") if self.options.get_safe("with_python", False): self.requires("pybind11/2.13.6") From 9977c60cb76416486667ecd3a95b5df3e198191e Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 14:47:15 +0200 Subject: [PATCH 2/2] fix(http-server): let a bind be retried after one failed cpp-httplib 0.47.0 decommissions the server when a bind fails - both `bind_to_port` and `bind_to_any_port` set `is_decommissioned`, and every later `bind_internal()` short-circuits on it - so the "try a preferred port, fall back to any port" pattern stopped working: the second `HttpServer::bind` threw `ServerBindFailed` forever. 0.16.3 has no such flag and allowed the retry, which makes this a regression the bump introduces rather than anything new in the wrapper. `Server::stop()` is what clears the flag, and with nothing listening it does nothing else, so the failing bind path calls it before throwing. Recreating the server object would have worked too but would drop the exception handler, the socket options and the mounted routes the constructor installs. `bind_can_be_retried_after_it_failed` fails with `server bind failed: 127.0.0.1:0` without the reset. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YSePnR4Jcj5cq3H1D11xcd --- src/odr/http_server.cpp | 3 +++ test/src/http_server_test.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/odr/http_server.cpp b/src/odr/http_server.cpp index 7367b3f1..bdcc4705 100644 --- a/src/odr/http_server.cpp +++ b/src/odr/http_server.cpp @@ -215,6 +215,9 @@ class HttpServer::Impl : public std::enable_shared_from_this { ? static_cast(port) : -1); if (bound < 0) { + // a failed bind decommissions the server and only stop() clears that, so + // without this no later bind - a fallback to any port - could succeed + m_server->stop(); throw ServerBindFailed(host, port); } diff --git a/test/src/http_server_test.cpp b/test/src/http_server_test.cpp index 8b3c86a6..25c99cd7 100644 --- a/test/src/http_server_test.cpp +++ b/test/src/http_server_test.cpp @@ -53,6 +53,20 @@ TEST(HttpServer, bind_reports_a_port_in_use) { taken.stop(); } +TEST(HttpServer, bind_can_be_retried_after_it_failed) { + const HttpServer taken; + const std::uint32_t port = taken.bind("127.0.0.1", 0); + + // falling back to any port is the reason a failed bind may not be terminal: + // cpp-httplib decommissions the server on one, and only stop() undoes that + const HttpServer other; + EXPECT_THROW(other.bind("127.0.0.1", port), ServerBindFailed); + EXPECT_NE(other.bind("127.0.0.1", 0), 0); + + other.stop(); + taken.stop(); +} + TEST(HttpServer, listen_without_bind_is_refused) { const HttpServer server;