@@ -48,7 +48,8 @@ CurlMultiManager::~CurlMultiManager() {
4848
4949void CurlMultiManager::add_handle (const std::shared_ptr<CURL >& easy,
5050 curl_slist* headers,
51- CompletionCallback callback) {
51+ CompletionCallback callback,
52+ std::optional<std::chrono::milliseconds> read_timeout) {
5253 if (const CURLMcode rc = curl_multi_add_handle (
5354 multi_handle_.get (), easy.get ());
5455 rc != CURLM_OK ) {
@@ -67,6 +68,24 @@ void CurlMultiManager::add_handle(const std::shared_ptr<CURL>& easy,
6768 callbacks_[easy.get ()] = std::move (callback);
6869 headers_[easy.get ()] = headers;
6970 handles_[easy.get ()] = easy;
71+
72+ // Setup read timeout timer if specified
73+ if (read_timeout) {
74+ auto timer = std::make_shared<boost::asio::steady_timer>(executor_);
75+ handle_timeouts_[easy.get ()] = HandleTimeoutInfo{read_timeout, timer};
76+
77+ // Start the timeout timer
78+ timer->expires_after (*read_timeout);
79+ auto weak_self = weak_from_this ();
80+ CURL * easy_ptr = easy.get ();
81+ timer->async_wait ([weak_self, easy_ptr](const boost::system::error_code& ec) {
82+ if (!ec) {
83+ if (auto self = weak_self.lock ()) {
84+ self->handle_read_timeout (easy_ptr);
85+ }
86+ }
87+ });
88+ }
7089 }
7190}
7291
@@ -79,6 +98,9 @@ int CurlMultiManager::socket_callback(CURL* easy,
7998
8099 std::lock_guard lock (manager->mutex_ );
81100
101+ // Reset read timeout on any socket activity
102+ manager->reset_read_timeout (easy);
103+
82104 if (what == CURL_POLL_REMOVE ) {
83105 // Remove socket from managed container
84106 if (const auto it = manager->sockets_ .find (s);
@@ -177,6 +199,13 @@ void CurlMultiManager::check_multi_info() {
177199 headers = header_it->second ;
178200 headers_.erase (header_it);
179201 }
202+
203+ // Cancel and remove timeout timer
204+ if (auto timeout_it = handle_timeouts_.find (easy);
205+ timeout_it != handle_timeouts_.end ()) {
206+ timeout_it->second .timer ->cancel ();
207+ handle_timeouts_.erase (timeout_it);
208+ }
180209 }
181210
182211 // Remove from multi handle
@@ -197,7 +226,7 @@ void CurlMultiManager::check_multi_info() {
197226 if (callback) {
198227 boost::asio::post (executor_, [callback = std::move (callback),
199228 result, handle]() {
200- callback (handle, result);
229+ callback (handle, Result::FromCurlCode ( result) );
201230 });
202231 }
203232 }
@@ -243,7 +272,7 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
243272 // Use weak_ptr in capture to avoid circular reference
244273 socket_info->read_handler = std::make_shared<std::function<void
245274 ()>>();
246- std::weak_ptr<std::function< void ()>> weak_read_handler = socket_info
275+ std::weak_ptr weak_read_handler = socket_info
247276 ->read_handler ;
248277 *socket_info->read_handler = [weak_self, sockfd, weak_handle,
249278 weak_read_handler]() {
@@ -328,6 +357,83 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
328357 }
329358 }
330359}
360+
361+ void CurlMultiManager::reset_read_timeout (CURL * easy) {
362+ // Must be called with mutex_ locked
363+ auto timeout_it = handle_timeouts_.find (easy);
364+ if (timeout_it != handle_timeouts_.end () && timeout_it->second .timer ) {
365+ auto & timeout_info = timeout_it->second ;
366+ timeout_info.timer ->cancel ();
367+ timeout_info.timer ->expires_after (*timeout_info.timeout_duration );
368+
369+ auto weak_self = weak_from_this ();
370+ CURL * easy_ptr = easy;
371+ timeout_info.timer ->async_wait ([weak_self, easy_ptr](const boost::system::error_code& ec) {
372+ if (!ec) {
373+ if (auto self = weak_self.lock ()) {
374+ self->handle_read_timeout (easy_ptr);
375+ }
376+ }
377+ });
378+ }
379+ }
380+
381+ void CurlMultiManager::handle_read_timeout (CURL * easy) {
382+ CompletionCallback callback;
383+ curl_slist* headers = nullptr ;
384+ std::shared_ptr<CURL > handle;
385+
386+ {
387+ std::lock_guard lock (mutex_);
388+
389+ // Check if handle still exists
390+ auto it = callbacks_.find (easy);
391+ if (it == callbacks_.end ()) {
392+ return ; // Handle already completed
393+ }
394+
395+ // Get and remove callback
396+ callback = std::move (it->second );
397+ callbacks_.erase (it);
398+
399+ // Get and remove headers
400+ if (auto header_it = headers_.find (easy);
401+ header_it != headers_.end ()) {
402+ headers = header_it->second ;
403+ headers_.erase (header_it);
404+ }
405+
406+ // Get and remove handle
407+ if (auto handle_it = handles_.find (easy);
408+ handle_it != handles_.end ()) {
409+ handle = handle_it->second ;
410+ handles_.erase (handle_it);
411+ }
412+
413+ // Remove timeout info
414+ if (auto timeout_it = handle_timeouts_.find (easy);
415+ timeout_it != handle_timeouts_.end ()) {
416+ timeout_it->second .timer ->cancel ();
417+ handle_timeouts_.erase (timeout_it);
418+ }
419+ }
420+
421+ // Remove from multi handle
422+ curl_multi_remove_handle (multi_handle_.get (), easy);
423+
424+ // Free headers
425+ if (headers) {
426+ curl_slist_free_all (headers);
427+ }
428+
429+ // Invoke completion callback with read timeout result
430+ if (callback) {
431+ boost::asio::post (executor_, [callback = std::move (callback),
432+ handle]() {
433+ callback (handle, Result::FromReadTimeout ());
434+ });
435+ }
436+ }
331437} // namespace launchdarkly::network
332438
333439#endif // LD_CURL_NETWORKING
0 commit comments