From de0f49e4fb904191df61f1802b708087f398665c Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 08:26:12 -0400 Subject: [PATCH 01/15] Fix #1601 --- httplib.h | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index cfeb051541..48f9ae33c3 100644 --- a/httplib.h +++ b/httplib.h @@ -341,6 +341,50 @@ using socket_t = int; */ namespace httplib { +// Timeout-enabled getaddrinfo for Issue #1601: Client Get operation stalls when network is down +inline int getaddrinfo_with_timeout(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res, + time_t timeout_sec) { + if (timeout_sec <= 0) { + // No timeout specified, use standard getaddrinfo + return getaddrinfo(node, service, hints, res); + } + + // Use thread-based timeout implementation for cross-platform compatibility + std::mutex result_mutex; + std::condition_variable result_cv; + auto completed = false; + auto result = EAI_SYSTEM; + struct addrinfo *result_addrinfo = nullptr; + + // Launch getaddrinfo in a separate thread + std::thread resolve_thread([&]() { + auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo); + + std::lock_guard lock(result_mutex); + result = thread_result; + completed = true; + result_cv.notify_one(); + }); + + // Wait for completion or timeout + std::unique_lock lock(result_mutex); + auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), + [&] { return completed; }); + + if (finished) { + // Operation completed within timeout + resolve_thread.join(); + *res = result_addrinfo; + return result; + } else { + // Timeout occurred + resolve_thread.detach(); // Let the thread finish in background + return EAI_AGAIN; // Return timeout error + } +} + namespace detail { /* @@ -3373,7 +3417,7 @@ template socket_t create_socket(const std::string &host, const std::string &ip, int port, int address_family, int socket_flags, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, - BindOrConnect bind_or_connect) { + BindOrConnect bind_or_connect, time_t timeout_sec = 0) { // Get address info const char *node = nullptr; struct addrinfo hints; @@ -3443,7 +3487,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, auto service = std::to_string(port); - if (getaddrinfo(node, service.c_str(), &hints, &result)) { + if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, timeout_sec)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif @@ -3541,7 +3585,7 @@ inline bool bind_ip_address(socket_t sock, const std::string &host) { hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; - if (getaddrinfo(host.c_str(), "0", &hints, &result)) { return false; } + if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { return false; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); auto ret = false; @@ -3646,7 +3690,7 @@ inline socket_t create_client_socket( error = Error::Success; return true; - }); + }, connection_timeout_sec); // Pass DNS timeout if (sock != INVALID_SOCKET) { error = Error::Success; @@ -5867,7 +5911,7 @@ inline void hosted_at(const std::string &hostname, hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; - if (getaddrinfo(hostname.c_str(), nullptr, &hints, &result)) { + if (getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, &result, 0)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif From cbfb0eb47aedec04fd89241b0ae0d0142179ed92 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 08:31:36 -0400 Subject: [PATCH 02/15] clang-format --- httplib.h | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/httplib.h b/httplib.h index 48f9ae33c3..7ed7faba92 100644 --- a/httplib.h +++ b/httplib.h @@ -341,11 +341,11 @@ using socket_t = int; */ namespace httplib { -// Timeout-enabled getaddrinfo for Issue #1601: Client Get operation stalls when network is down +// Timeout-enabled getaddrinfo for Issue #1601: Client Get operation stalls when +// network is down inline int getaddrinfo_with_timeout(const char *node, const char *service, const struct addrinfo *hints, - struct addrinfo **res, - time_t timeout_sec) { + struct addrinfo **res, time_t timeout_sec) { if (timeout_sec <= 0) { // No timeout specified, use standard getaddrinfo return getaddrinfo(node, service, hints, res); @@ -361,7 +361,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, // Launch getaddrinfo in a separate thread std::thread resolve_thread([&]() { auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo); - + std::lock_guard lock(result_mutex); result = thread_result; completed = true; @@ -370,7 +370,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, // Wait for completion or timeout std::unique_lock lock(result_mutex); - auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), + auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), [&] { return completed; }); if (finished) { @@ -381,7 +381,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } else { // Timeout occurred resolve_thread.detach(); // Let the thread finish in background - return EAI_AGAIN; // Return timeout error + return EAI_AGAIN; // Return timeout error } } @@ -3487,7 +3487,8 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, auto service = std::to_string(port); - if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, timeout_sec)) { + if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, + timeout_sec)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif @@ -3585,7 +3586,9 @@ inline bool bind_ip_address(socket_t sock, const std::string &host) { hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; - if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { return false; } + if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { + return false; + } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); auto ret = false; @@ -3690,7 +3693,8 @@ inline socket_t create_client_socket( error = Error::Success; return true; - }, connection_timeout_sec); // Pass DNS timeout + }, + connection_timeout_sec); // Pass DNS timeout if (sock != INVALID_SOCKET) { error = Error::Success; From 25877944f96392e43cf9ca803581fa50bfff4647 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 08:44:24 -0400 Subject: [PATCH 03/15] Fix Windows problem --- httplib.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/httplib.h b/httplib.h index 7ed7faba92..3f9828973a 100644 --- a/httplib.h +++ b/httplib.h @@ -341,6 +341,16 @@ using socket_t = int; */ namespace httplib { +// Windows compatibility for getaddrinfo error codes +#ifdef _WIN32 +#ifndef EAI_SYSTEM +#define EAI_SYSTEM WSANO_RECOVERY +#endif +#ifndef EAI_AGAIN +#define EAI_AGAIN WSATRY_AGAIN +#endif +#endif + // Timeout-enabled getaddrinfo for Issue #1601: Client Get operation stalls when // network is down inline int getaddrinfo_with_timeout(const char *node, const char *service, From f4b7a13fd28a1fa47b924c0282538f1b2a92d0e6 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 09:21:25 -0400 Subject: [PATCH 04/15] Use GetAddrInfoEx on Windows --- httplib.h | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 3f9828973a..82b6353fb0 100644 --- a/httplib.h +++ b/httplib.h @@ -361,7 +361,68 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, return getaddrinfo(node, service, hints, res); } - // Use thread-based timeout implementation for cross-platform compatibility +#ifdef _WIN32 + // Windows-specific implementation using GetAddrInfoEx with overlapped I/O + OVERLAPPED overlapped = {0}; + HANDLE event = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!event) { + return EAI_FAIL; + } + + overlapped.hEvent = event; + + ADDRINFOEX *result_addrinfo = nullptr; + HANDLE cancel_handle = nullptr; + + // Convert struct addrinfo* to ADDRINFOEX* hints if provided + ADDRINFOEX hints_ex = {0}; + if (hints) { + hints_ex.ai_flags = hints->ai_flags; + hints_ex.ai_family = hints->ai_family; + hints_ex.ai_socktype = hints->ai_socktype; + hints_ex.ai_protocol = hints->ai_protocol; + } + + int ret = GetAddrInfoEx( + node, + service, + NS_DNS, + NULL, + hints ? &hints_ex : NULL, + &result_addrinfo, + NULL, + &overlapped, + NULL, + &cancel_handle + ); + + if (ret == WSA_IO_PENDING) { + DWORD wait_result = WaitForSingleObject(event, static_cast(timeout_sec * 1000)); + if (wait_result == WAIT_TIMEOUT) { + if (cancel_handle) { + GetAddrInfoExCancel(&cancel_handle); + } + CloseHandle(event); + return EAI_AGAIN; + } + + DWORD bytes_returned; + if (!GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, &bytes_returned, FALSE)) { + CloseHandle(event); + return WSAGetLastError(); + } + } + + CloseHandle(event); + + if (ret == NO_ERROR) { + *res = reinterpret_cast(result_addrinfo); + return 0; + } + + return ret; +#else + // Unix/Linux implementation using thread-based timeout std::mutex result_mutex; std::condition_variable result_cv; auto completed = false; @@ -393,6 +454,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, resolve_thread.detach(); // Let the thread finish in background return EAI_AGAIN; // Return timeout error } +#endif } namespace detail { From bd32a0f32021f7581509c167fe078dc95467f5b9 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 16:53:35 -0400 Subject: [PATCH 05/15] Fix Windows problem --- httplib.h | 213 ++++++++++++++++++++++++------------------------------ 1 file changed, 96 insertions(+), 117 deletions(-) diff --git a/httplib.h b/httplib.h index 82b6353fb0..44145ea33c 100644 --- a/httplib.h +++ b/httplib.h @@ -341,122 +341,6 @@ using socket_t = int; */ namespace httplib { -// Windows compatibility for getaddrinfo error codes -#ifdef _WIN32 -#ifndef EAI_SYSTEM -#define EAI_SYSTEM WSANO_RECOVERY -#endif -#ifndef EAI_AGAIN -#define EAI_AGAIN WSATRY_AGAIN -#endif -#endif - -// Timeout-enabled getaddrinfo for Issue #1601: Client Get operation stalls when -// network is down -inline int getaddrinfo_with_timeout(const char *node, const char *service, - const struct addrinfo *hints, - struct addrinfo **res, time_t timeout_sec) { - if (timeout_sec <= 0) { - // No timeout specified, use standard getaddrinfo - return getaddrinfo(node, service, hints, res); - } - -#ifdef _WIN32 - // Windows-specific implementation using GetAddrInfoEx with overlapped I/O - OVERLAPPED overlapped = {0}; - HANDLE event = CreateEvent(NULL, TRUE, FALSE, NULL); - if (!event) { - return EAI_FAIL; - } - - overlapped.hEvent = event; - - ADDRINFOEX *result_addrinfo = nullptr; - HANDLE cancel_handle = nullptr; - - // Convert struct addrinfo* to ADDRINFOEX* hints if provided - ADDRINFOEX hints_ex = {0}; - if (hints) { - hints_ex.ai_flags = hints->ai_flags; - hints_ex.ai_family = hints->ai_family; - hints_ex.ai_socktype = hints->ai_socktype; - hints_ex.ai_protocol = hints->ai_protocol; - } - - int ret = GetAddrInfoEx( - node, - service, - NS_DNS, - NULL, - hints ? &hints_ex : NULL, - &result_addrinfo, - NULL, - &overlapped, - NULL, - &cancel_handle - ); - - if (ret == WSA_IO_PENDING) { - DWORD wait_result = WaitForSingleObject(event, static_cast(timeout_sec * 1000)); - if (wait_result == WAIT_TIMEOUT) { - if (cancel_handle) { - GetAddrInfoExCancel(&cancel_handle); - } - CloseHandle(event); - return EAI_AGAIN; - } - - DWORD bytes_returned; - if (!GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, &bytes_returned, FALSE)) { - CloseHandle(event); - return WSAGetLastError(); - } - } - - CloseHandle(event); - - if (ret == NO_ERROR) { - *res = reinterpret_cast(result_addrinfo); - return 0; - } - - return ret; -#else - // Unix/Linux implementation using thread-based timeout - std::mutex result_mutex; - std::condition_variable result_cv; - auto completed = false; - auto result = EAI_SYSTEM; - struct addrinfo *result_addrinfo = nullptr; - - // Launch getaddrinfo in a separate thread - std::thread resolve_thread([&]() { - auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo); - - std::lock_guard lock(result_mutex); - result = thread_result; - completed = true; - result_cv.notify_one(); - }); - - // Wait for completion or timeout - std::unique_lock lock(result_mutex); - auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), - [&] { return completed; }); - - if (finished) { - // Operation completed within timeout - resolve_thread.join(); - *res = result_addrinfo; - return result; - } else { - // Timeout occurred - resolve_thread.detach(); // Let the thread finish in background - return EAI_AGAIN; // Return timeout error - } -#endif -} - namespace detail { /* @@ -3485,6 +3369,100 @@ unescape_abstract_namespace_unix_domain(const std::string &s) { return s; } +inline int getaddrinfo_with_timeout(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res, time_t timeout_sec) { + if (timeout_sec <= 0) { + // No timeout specified, use standard getaddrinfo + return getaddrinfo(node, service, hints, res); + } + +#ifdef _WIN32 + // Windows-specific implementation using GetAddrInfoEx with overlapped I/O + OVERLAPPED overlapped = {0}; + HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!event) { return EAI_FAIL; } + + overlapped.hEvent = event; + + PADDRINFOEXW result_addrinfo = nullptr; + HANDLE cancel_handle = nullptr; + + ADDRINFOEXW hints_ex = {0}; + if (hints) { + hints_ex.ai_flags = hints->ai_flags; + hints_ex.ai_family = hints->ai_family; + hints_ex.ai_socktype = hints->ai_socktype; + hints_ex.ai_protocol = hints->ai_protocol; + } + + auto wnode = u8string_to_wstring(node); + auto wservice = u8string_to_wstring(service); + + auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr, + hints ? &hints_ex : nullptr, &result_addrinfo, + nullptr, &overlapped, nullptr, &cancel_handle); + + if (ret == WSA_IO_PENDING) { + auto wait_result = + ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); + if (wait_result == WAIT_TIMEOUT) { + if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } + ::CloseHandle(event); + return EAI_AGAIN; + } + + DWORD bytes_returned; + if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, + &bytes_returned, FALSE)) { + ::CloseHandle(event); + return ::WSAGetLastError(); + } + } + + ::CloseHandle(event); + + if (ret == NO_ERROR || ret == WSA_IO_PENDING) { + *res = reinterpret_cast(result_addrinfo); + return 0; + } + + return ret; +#else + // macOS/Linux implementation using thread-based timeout + std::mutex result_mutex; + std::condition_variable result_cv; + auto completed = false; + auto result = EAI_SYSTEM; + struct addrinfo *result_addrinfo = nullptr; + + std::thread resolve_thread([&]() { + auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo); + + std::lock_guard lock(result_mutex); + result = thread_result; + completed = true; + result_cv.notify_one(); + }); + + // Wait for completion or timeout + std::unique_lock lock(result_mutex); + auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), + [&] { return completed; }); + + if (finished) { + // Operation completed within timeout + resolve_thread.join(); + *res = result_addrinfo; + return result; + } else { + // Timeout occurred + resolve_thread.detach(); // Let the thread finish in background + return EAI_AGAIN; // Return timeout error + } +#endif +} + template socket_t create_socket(const std::string &host, const std::string &ip, int port, int address_family, int socket_flags, bool tcp_nodelay, @@ -5987,7 +5965,8 @@ inline void hosted_at(const std::string &hostname, hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; - if (getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, &result, 0)) { + if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, + &result, 0)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif From bce52de983868f5f7615c0825049d459fbe75373 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 20:57:15 -0400 Subject: [PATCH 06/15] Add getaddrinfo_a --- httplib.h | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 44145ea33c..d316fd63bc 100644 --- a/httplib.h +++ b/httplib.h @@ -3428,8 +3428,60 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } return ret; +#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) + // Linux implementation using getaddrinfo_a for asynchronous DNS resolution + struct gaicb request; + struct gaicb *requests[1] = {&request}; + struct sigevent sevp; + struct timespec timeout; + + // Initialize the request structure + memset(&request, 0, sizeof(request)); + request.ar_name = node; + request.ar_service = service; + request.ar_request = hints; + + // Set up timeout + timeout.tv_sec = timeout_sec; + timeout.tv_nsec = 0; + + // Initialize sigevent structure (not used, but required) + memset(&sevp, 0, sizeof(sevp)); + sevp.sigev_notify = SIGEV_NONE; + + // Start asynchronous resolution + int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); + if (start_result != 0) { + return start_result; + } + + // Wait for completion with timeout + int wait_result = gai_suspend((const struct gaicb * const *)requests, 1, &timeout); + + if (wait_result == 0) { + // Completed successfully, get the result + int gai_result = gai_error(&request); + if (gai_result == 0) { + *res = request.ar_result; + return 0; + } else { + // Clean up on error + if (request.ar_result) { + freeaddrinfo(request.ar_result); + } + return gai_result; + } + } else if (wait_result == EAI_AGAIN) { + // Timeout occurred, cancel the request + gai_cancel(&request); + return EAI_AGAIN; + } else { + // Other error occurred + gai_cancel(&request); + return wait_result; + } #else - // macOS/Linux implementation using thread-based timeout + // Fallback implementation using thread-based timeout for other Unix systems std::mutex result_mutex; std::condition_variable result_cv; auto completed = false; From d31f75820376a296efefd941a98b3ec4850ce658 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 21:19:06 -0400 Subject: [PATCH 07/15] clang-format --- httplib.h | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/httplib.h b/httplib.h index d316fd63bc..df0c68e9fb 100644 --- a/httplib.h +++ b/httplib.h @@ -3428,36 +3428,36 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } return ret; -#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) +#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ + (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) // Linux implementation using getaddrinfo_a for asynchronous DNS resolution struct gaicb request; struct gaicb *requests[1] = {&request}; struct sigevent sevp; struct timespec timeout; - + // Initialize the request structure memset(&request, 0, sizeof(request)); request.ar_name = node; request.ar_service = service; request.ar_request = hints; - + // Set up timeout timeout.tv_sec = timeout_sec; timeout.tv_nsec = 0; - + // Initialize sigevent structure (not used, but required) memset(&sevp, 0, sizeof(sevp)); sevp.sigev_notify = SIGEV_NONE; - + // Start asynchronous resolution int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); - if (start_result != 0) { - return start_result; - } - + if (start_result != 0) { return start_result; } + // Wait for completion with timeout - int wait_result = gai_suspend((const struct gaicb * const *)requests, 1, &timeout); - + int wait_result = + gai_suspend((const struct gaicb *const *)requests, 1, &timeout); + if (wait_result == 0) { // Completed successfully, get the result int gai_result = gai_error(&request); @@ -3466,9 +3466,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, return 0; } else { // Clean up on error - if (request.ar_result) { - freeaddrinfo(request.ar_result); - } + if (request.ar_result) { freeaddrinfo(request.ar_result); } return gai_result; } } else if (wait_result == EAI_AGAIN) { From 7937ecb786a10a474094edbed3bfbd3b75373931 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 22:37:40 -0400 Subject: [PATCH 08/15] Adjust Benchmark Test --- test/test.cc | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/test.cc b/test/test.cc index 204c2d2805..c5f10cc31e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3388,16 +3388,19 @@ void performance_test(const char *host) { Client cli(host, port); + const int NUM_WARMUP = 10; const int NUM_REQUESTS = 50; const int MAX_AVERAGE_MS = 5; - auto warmup = cli.Get("/benchmark"); - ASSERT_TRUE(warmup); + for (int i = 0; i < NUM_WARMUP; ++i) { + auto warmup = cli.Get("/benchmark"); + ASSERT_TRUE(warmup); + } auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < NUM_REQUESTS; ++i) { auto res = cli.Get("/benchmark"); - ASSERT_TRUE(res) << "Request " << i << " failed"; + ASSERT_TRUE(res); EXPECT_EQ(StatusCode::OK_200, res->status); } auto end = std::chrono::high_resolution_clock::now(); @@ -3407,10 +3410,6 @@ void performance_test(const char *host) { .count(); double avg_ms = static_cast(total_ms) / NUM_REQUESTS; - std::cout << "Peformance test at \"" << host << "\": " << NUM_REQUESTS - << " requests in " << total_ms << "ms (avg: " << avg_ms << "ms)" - << std::endl; - EXPECT_LE(avg_ms, MAX_AVERAGE_MS) << "Performance is too slow: " << avg_ms << "ms (Issue #1777)"; } From f92f9e65c5b40f73879aee4995ae682efc3f3231 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:06:25 -0400 Subject: [PATCH 09/15] Test --- test/test.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test.cc b/test/test.cc index c5f10cc31e..043646c054 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3388,8 +3388,11 @@ void performance_test(const char *host) { Client cli(host, port); - const int NUM_WARMUP = 10; - const int NUM_REQUESTS = 50; + // const int NUM_WARMUP = 10; + // const int NUM_REQUESTS = 50; + // const int MAX_AVERAGE_MS = 5; + const int NUM_WARMUP = 0; + const int NUM_REQUESTS = 1; const int MAX_AVERAGE_MS = 5; for (int i = 0; i < NUM_WARMUP; ++i) { From ede5c934386ffbbba0a35d0782e1b94efef9951d Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:16:47 -0400 Subject: [PATCH 10/15] Fix Bench test --- test/test.cc | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/test/test.cc b/test/test.cc index 043646c054..ba3520334a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3388,24 +3388,12 @@ void performance_test(const char *host) { Client cli(host, port); - // const int NUM_WARMUP = 10; - // const int NUM_REQUESTS = 50; - // const int MAX_AVERAGE_MS = 5; - const int NUM_WARMUP = 0; - const int NUM_REQUESTS = 1; - const int MAX_AVERAGE_MS = 5; + auto start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < NUM_WARMUP; ++i) { - auto warmup = cli.Get("/benchmark"); - ASSERT_TRUE(warmup); - } + auto res = cli.Get("/benchmark"); + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); - auto start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < NUM_REQUESTS; ++i) { - auto res = cli.Get("/benchmark"); - ASSERT_TRUE(res); - EXPECT_EQ(StatusCode::OK_200, res->status); - } auto end = std::chrono::high_resolution_clock::now(); auto total_ms = @@ -3413,8 +3401,8 @@ void performance_test(const char *host) { .count(); double avg_ms = static_cast(total_ms) / NUM_REQUESTS; - EXPECT_LE(avg_ms, MAX_AVERAGE_MS) - << "Performance is too slow: " << avg_ms << "ms (Issue #1777)"; + EXPECT_LE(avg_ms, 5) << "Performance is too slow: " << avg_ms + << "ms (Issue #1777)"; } TEST(BenchmarkTest, localhost) { performance_test("localhost"); } From 35a18f34277245bf5a3b932eb26e005541530157 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:27:12 -0400 Subject: [PATCH 11/15] Fix build error --- test/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Makefile b/test/Makefile index 3107702beb..322def05a5 100644 --- a/test/Makefile +++ b/test/Makefile @@ -9,7 +9,7 @@ OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPEN ifneq ($(OS), Windows_NT) UNAME_S := $(shell uname -s) ifeq ($(UNAME_S), Darwin) - OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework CoreFoundation -framework Security + OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework CoreFoundation -framework Security -framework CFNetwork endif endif @@ -86,7 +86,7 @@ fuzz_test: server_fuzzer # Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use. server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o - $(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread + $(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl @file $@ # Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and From 576cc330afc3217dade8af65bd1358e67478ac15 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:35:54 -0400 Subject: [PATCH 12/15] Fix build error --- httplib.h | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index df0c68e9fb..ad03015339 100644 --- a/httplib.h +++ b/httplib.h @@ -278,6 +278,14 @@ using socket_t = int; #include #include +#if defined(__APPLE__) +#include +#if TARGET_OS_OSX || TARGET_OS_IPHONE +#include +#include +#endif +#endif + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 #include @@ -292,13 +300,16 @@ using socket_t = int; #ifdef _MSC_VER #pragma comment(lib, "crypt32.lib") #endif -#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#endif // _WIN32 + +#if defined(__APPLE__) +#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) #include #if TARGET_OS_OSX -#include #include #endif // TARGET_OS_OSX -#endif // _WIN32 +#endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN +#endif // ___APPLE__ #include #include @@ -321,7 +332,7 @@ using socket_t = int; #error Sorry, OpenSSL versions prior to 3.0.0 are not supported #endif -#endif +#endif // CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_ZLIB_SUPPORT #include @@ -3428,6 +3439,174 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } return ret; +#elif defined(__APPLE__) + // macOS implementation using CFHost API for asynchronous DNS resolution + CFStringRef hostname_ref = CFStringCreateWithCString( + kCFAllocatorDefault, node, kCFStringEncodingUTF8); + if (!hostname_ref) { return EAI_MEMORY; } + + CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref); + CFRelease(hostname_ref); + if (!host_ref) { return EAI_MEMORY; } + + // Set up context for callback + struct CFHostContext { + bool completed = false; + bool success = false; + CFArrayRef addresses = nullptr; + std::mutex mutex; + std::condition_variable cv; + } context; + + CFHostClientContext client_context; + memset(&client_context, 0, sizeof(client_context)); + client_context.info = &context; + + // Set callback + auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/, + const CFStreamError *error, void *info) { + auto ctx = static_cast(info); + std::lock_guard lock(ctx->mutex); + + if (error && error->error != 0) { + ctx->success = false; + } else { + Boolean hasBeenResolved; + ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved); + if (ctx->addresses && hasBeenResolved) { + CFRetain(ctx->addresses); + ctx->success = true; + } else { + ctx->success = false; + } + } + ctx->completed = true; + ctx->cv.notify_one(); + }; + + if (!CFHostSetClient(host_ref, callback, &client_context)) { + CFRelease(host_ref); + return EAI_SYSTEM; + } + + // Schedule on run loop + CFRunLoopRef run_loop = CFRunLoopGetCurrent(); + CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + + // Start resolution + CFStreamError stream_error; + if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) { + CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + CFRelease(host_ref); + return EAI_FAIL; + } + + // Wait for completion with timeout + auto timeout_time = + std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec); + bool timed_out = false; + + { + std::unique_lock lock(context.mutex); + + while (!context.completed) { + auto now = std::chrono::steady_clock::now(); + if (now >= timeout_time) { + timed_out = true; + break; + } + + // Run the runloop for a short time + lock.unlock(); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); + lock.lock(); + } + } + + // Clean up + CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + CFHostSetClient(host_ref, nullptr, nullptr); + + if (timed_out || !context.completed) { + CFHostCancelInfoResolution(host_ref, kCFHostAddresses); + CFRelease(host_ref); + return EAI_AGAIN; + } + + if (!context.success || !context.addresses) { + CFRelease(host_ref); + return EAI_NODATA; + } + + // Convert CFArray to addrinfo + CFIndex count = CFArrayGetCount(context.addresses); + if (count == 0) { + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_NODATA; + } + + struct addrinfo *result_addrinfo = nullptr; + struct addrinfo **current = &result_addrinfo; + + for (CFIndex i = 0; i < count; i++) { + CFDataRef addr_data = + static_cast(CFArrayGetValueAtIndex(context.addresses, i)); + if (!addr_data) continue; + + const struct sockaddr *sockaddr_ptr = + reinterpret_cast(CFDataGetBytePtr(addr_data)); + socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data)); + + // Allocate addrinfo structure + *current = static_cast(malloc(sizeof(struct addrinfo))); + if (!*current) { + freeaddrinfo(result_addrinfo); + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_MEMORY; + } + + memset(*current, 0, sizeof(struct addrinfo)); + + // Set up addrinfo fields + (*current)->ai_family = sockaddr_ptr->sa_family; + (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; + (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP; + (*current)->ai_addrlen = sockaddr_len; + + // Copy sockaddr + (*current)->ai_addr = static_cast(malloc(sockaddr_len)); + if (!(*current)->ai_addr) { + freeaddrinfo(result_addrinfo); + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_MEMORY; + } + memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len); + + // Set port if service is specified + if (service && strlen(service) > 0) { + int port = atoi(service); + if (port > 0) { + if (sockaddr_ptr->sa_family == AF_INET) { + reinterpret_cast((*current)->ai_addr) + ->sin_port = htons(static_cast(port)); + } else if (sockaddr_ptr->sa_family == AF_INET6) { + reinterpret_cast((*current)->ai_addr) + ->sin6_port = htons(static_cast(port)); + } + } + } + + current = &((*current)->ai_next); + } + + CFRelease(context.addresses); + CFRelease(host_ref); + + *res = result_addrinfo; + return 0; #elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) // Linux implementation using getaddrinfo_a for asynchronous DNS resolution From 3c4df8a08d2e6014a146e63ce71a9d9b7f490bbb Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:45:48 -0400 Subject: [PATCH 13/15] Fix Makefile --- test/Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/Makefile b/test/Makefile index 322def05a5..8b369917d2 100644 --- a/test/Makefile +++ b/test/Makefile @@ -21,7 +21,15 @@ BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_ ZSTD_DIR = $(PREFIX)/opt/zstd ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd -TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) -pthread -lcurl +LIBS = -lpthread -lcurl +ifneq ($(OS), Windows_NT) + UNAME_S := $(shell uname -s) + ifneq ($(UNAME_S), Darwin) + LIBS += -lanl + endif +endif + +TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS) # By default, use standalone_fuzz_target_runner. # This runner does no fuzzing, but simply executes the inputs @@ -86,7 +94,7 @@ fuzz_test: server_fuzzer # Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use. server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o - $(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl + $(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS) @file $@ # Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and From 05e023cb23d51874eb59d05c112c4cc484e313e5 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Jun 2025 23:49:48 -0400 Subject: [PATCH 14/15] Fix build error --- test/test.cc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test.cc b/test/test.cc index ba3520334a..d50f292f3a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3396,13 +3396,12 @@ void performance_test(const char *host) { auto end = std::chrono::high_resolution_clock::now(); - auto total_ms = + auto elapsed = std::chrono::duration_cast(end - start) .count(); - double avg_ms = static_cast(total_ms) / NUM_REQUESTS; - EXPECT_LE(avg_ms, 5) << "Performance is too slow: " << avg_ms - << "ms (Issue #1777)"; + EXPECT_LE(elapsed, 5) << "Performance is too slow: " << elapsed + << "ms (Issue #1777)"; } TEST(BenchmarkTest, localhost) { performance_test("localhost"); } From 5220889c8e1ab0239a652bc626f3c9feb9543c75 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 29 Jun 2025 00:01:48 -0400 Subject: [PATCH 15/15] Fix buid error --- test/fuzzing/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fuzzing/Makefile b/test/fuzzing/Makefile index d6a3e21bc1..b08ecd0848 100644 --- a/test/fuzzing/Makefile +++ b/test/fuzzing/Makefile @@ -20,7 +20,7 @@ all : server_fuzzer # Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use. server_fuzzer : server_fuzzer.cc ../../httplib.h # $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread - $(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread + $(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl zip -q -r server_fuzzer_seed_corpus.zip corpus clean: