From f883d0a627810a501b02e012b247e6e499e16898 Mon Sep 17 00:00:00 2001 From: H-D-OWL Date: Sun, 12 Jul 2026 19:33:21 +0500 Subject: [PATCH] Added request cancellation functionality to the HTTP client and modified the CurlHttpClient.cpp and Api.cpp files to ensure proper operation. --- include/tgbot/net/HttpClient.h | 51 +++++++++++++++++++++++++++++++--- src/Api.cpp | 23 ++++++++++++++- src/net/CurlHttpClient.cpp | 37 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/include/tgbot/net/HttpClient.h b/include/tgbot/net/HttpClient.h index 1f1b2a267..d7fa10b18 100644 --- a/include/tgbot/net/HttpClient.h +++ b/include/tgbot/net/HttpClient.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace TgBot { @@ -31,22 +32,64 @@ class TGBOT_API HttpClient { std::int32_t _timeout = 25; /** - * @brief Get the maximum number of makeRequest() retries before giving up and throwing an exception. - */ + * @brief Get the maximum number of makeRequest() retries before giving up and throwing an exception. + */ virtual int getRequestMaxRetries() const { return requestMaxRetries; } /** - * @brief Get the makeRequest() backoff duration between retries, in seconds. - */ + * @brief Get the makeRequest() backoff duration between retries, in seconds. + */ virtual int getRequestBackoff() const { return requestBackoff; } + /** + * @brief Cancels the requests. + * + * @param eternal Optional. If true, permanently disables the HTTP client, canceling all current and future requests. + * If false, cancel the currently running requests. + */ + virtual void cancel(const bool eternal = false) const { + if (eternal) { + _isEternalCancel.store(true); + } + else { + _cancelEpoch.fetch_add(1); + } + } + + /** + * @brief Checks if the HTTP client is permanently disabled. + */ + virtual bool isEternalCancelled() const { + return _isEternalCancel.load(); + } + + /** + * @brief Get the exception message that occurs when the request is aborted. + */ + virtual const std::string& getCancelExceptionText() const { + return cancelExceptionText; + } + +protected: + + /** + * @brief Flag indicating whether the HTTP client is permanently disabled. + */ + mutable std::atomic _isEternalCancel{ false }; + + /** + * @brief Counter used to invalidate current requests. + */ + mutable std::atomic _cancelEpoch{ 0 }; + private: int requestMaxRetries = 3; int requestBackoff = 1; + const std::string cancelExceptionText = "request cancelled"; }; } diff --git a/src/Api.cpp b/src/Api.cpp index 9c2083033..dc689ad9e 100644 --- a/src/Api.cpp +++ b/src/Api.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace TgBot { @@ -2863,11 +2864,31 @@ boost::property_tree::ptree Api::sendRequest(const std::string& method, const st throw TgException(message, static_cast(errorCode)); } } catch (...) { + bool isCancelException = false; + + try { + throw; + } + catch (const std::exception& e) { + const std::string_view sv{e.what()}; + + if(sv.compare(_httpClient.getCancelExceptionText()) == 0) { + isCancelException = true; + } + } + catch (...) { + } + int max_retries = _httpClient.getRequestMaxRetries(); - if ((max_retries >= 0) && (retries == max_retries)) { + if (isCancelException || _httpClient.isEternalCancelled() || ((max_retries >= 0) && (retries == max_retries))) { throw; } else { std::this_thread::sleep_for(std::chrono::seconds(requestRetryBackoff)); + + if (_httpClient.isEternalCancelled()) { + throw; + } + retries++; continue; } diff --git a/src/net/CurlHttpClient.cpp b/src/net/CurlHttpClient.cpp index 39f3956b2..d69fffb9b 100644 --- a/src/net/CurlHttpClient.cpp +++ b/src/net/CurlHttpClient.cpp @@ -4,9 +4,24 @@ #include #include +#include +#include +#include namespace TgBot { + namespace { + + struct RequestCancelState { + RequestCancelState(const std::atomic* const isEternalCancel, const std::atomic* const globalCancelEpoch, const uint64_t currentCancelEpoch) + : isEternalCancel(isEternalCancel), globalCancelEpoch(globalCancelEpoch), currentCancelEpoch(currentCancelEpoch) {} + + const std::atomic* const isEternalCancel = nullptr; + const std::atomic* const globalCancelEpoch = nullptr; + const uint64_t currentCancelEpoch = 0; + }; + } + CurlHttpClient::CurlHttpClient() : _httpParser() { } @@ -35,6 +50,16 @@ static CURL* getCurlHandle(const CurlHttpClient *c_) { return it->second; } +static int curlProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + const RequestCancelState* const state = static_cast(clientp); + if (state && ((state->isEternalCancel && state->isEternalCancel->load()) || (state->globalCancelEpoch && state->currentCancelEpoch < state->globalCancelEpoch->load()))) { + return 1; + } + + return 0; +} + static std::size_t curlWriteString(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) { static_cast(userdata)->append(ptr, size * nmemb); return size * nmemb; @@ -48,6 +73,12 @@ std::string CurlHttpClient::makeRequest(const Url& url, const std::vectorload()) || (state.globalCancelEpoch && state.currentCancelEpoch < state.globalCancelEpoch->load()))) { + const size_t slashPos = url.path.rfind('/'); + + throw std::runtime_error(slashPos == std::string::npos ? getCancelExceptionText() : getCancelExceptionText() + ": " + url.path.substr(slashPos + 1)); + } + // If the request did not complete correctly, show the error // information. If no detailed error information was written to errbuf // show the more generic information from curl_easy_strerror instead.