Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions include/tgbot/net/HttpClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include <cstdint>
#include <atomic>

namespace TgBot {

Expand All @@ -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<bool> _isEternalCancel{ false };

/**
* @brief Counter used to invalidate current requests.
*/
mutable std::atomic<uint64_t> _cancelEpoch{ 0 };

private:
int requestMaxRetries = 3;
int requestBackoff = 1;
const std::string cancelExceptionText = "request cancelled";
};

}
Expand Down
23 changes: 22 additions & 1 deletion src/Api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <chrono>
#include <thread>
#include <string_view>

namespace TgBot {

Expand Down Expand Up @@ -2863,11 +2864,31 @@ boost::property_tree::ptree Api::sendRequest(const std::string& method, const st
throw TgException(message, static_cast<TgException::ErrorCode>(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;
}
Expand Down
37 changes: 37 additions & 0 deletions src/net/CurlHttpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,24 @@

#include <cstddef>
#include <string>
#include <atomic>
#include <cstdint>
#include <cstring>

namespace TgBot {

namespace {

struct RequestCancelState {
RequestCancelState(const std::atomic<bool>* const isEternalCancel, const std::atomic<uint64_t>* const globalCancelEpoch, const uint64_t currentCancelEpoch)
: isEternalCancel(isEternalCancel), globalCancelEpoch(globalCancelEpoch), currentCancelEpoch(currentCancelEpoch) {}

const std::atomic<bool>* const isEternalCancel = nullptr;
const std::atomic<uint64_t>* const globalCancelEpoch = nullptr;
const uint64_t currentCancelEpoch = 0;
};
}

CurlHttpClient::CurlHttpClient() : _httpParser() {
}

Expand Down Expand Up @@ -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<const RequestCancelState* const>(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<std::string*>(userdata)->append(ptr, size * nmemb);
return size * nmemb;
Expand All @@ -48,6 +73,12 @@ std::string CurlHttpClient::makeRequest(const Url& url, const std::vector<HttpRe
curl_easy_setopt(curl, CURLOPT_TIMEOUT, _timeout);
curl_easy_setopt(curl, CURLOPT_PROXY, _proxyUrl);

const RequestCancelState state{ &_isEternalCancel, &_cancelEpoch, _cancelEpoch.load() };

curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, curlProgressCallback);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &state);

std::string u = url.protocol + "://" + url.host + url.path;
if (args.empty()) {
u += "?" + url.query;
Expand Down Expand Up @@ -81,6 +112,12 @@ std::string CurlHttpClient::makeRequest(const Url& url, const std::vector<HttpRe
auto res = curl_easy_perform(curl);
curl_mime_free(mime);

if (res == CURLcode::CURLE_ABORTED_BY_CALLBACK && ((state.isEternalCancel && state.isEternalCancel->load()) || (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.
Expand Down
Loading