34size_t WriteCallback(
void* contents,
size_t size,
size_t nmemb,
void* userp) {
35 static_cast<std::string*
>(userp)->append(
static_cast<char*
>(contents), size * nmemb);
39int XferInfoCallback(
void* clientp, curl_off_t , curl_off_t , curl_off_t ,
41 const auto* token =
static_cast<const std::stop_token*
>(clientp);
42 return (token !=
nullptr && token->stop_requested()) ? 1 : 0;
49size_t HeaderCallback(
char* buffer,
size_t size,
size_t nitems,
void* userdata) {
50 const size_t total = size * nitems;
51 auto* state =
static_cast<HeaderState*
>(userdata);
52 const std::string_view header(buffer, total);
55 constexpr std::string_view kKey =
"x-ratelimit-remaining:";
56 if (header.size() >= kKey.size()) {
58 lower.reserve(kKey.size());
59 for (
size_t i = 0; i < kKey.size() && i < header.size(); ++i) {
60 lower.push_back(
static_cast<char>(std::tolower(
static_cast<unsigned char>(header[i]))));
63 std::string_view value = header.substr(kKey.size());
64 while (!value.empty() && (value.front() ==
' ' || value.front() ==
'\t')) {
65 value.remove_prefix(1);
67 if (!value.empty() && value.front() ==
'0') {
68 state->rateLimited =
true;
82 const int totalAttempts = std::max(1, request.
maxRetries + 1);
83 for (
int attempt = 0; attempt < totalAttempts; ++attempt) {
84 if (stopToken.stop_requested()) {
89 response = PerformOnce(request, stopToken);
97 if (attempt + 1 >= totalAttempts) {
103 const auto backoff = std::chrono::milliseconds(200 << attempt);
104 const auto deadline = std::chrono::steady_clock::now() + backoff;
105 while (std::chrono::steady_clock::now() < deadline) {
106 if (stopToken.stop_requested()) {
110 std::this_thread::sleep_for(std::chrono::milliseconds(50));
118 return Get(request, std::stop_token{});
124 const std::unique_ptr<CURL,
decltype(&curl_easy_cleanup)> curl(curl_easy_init(), curl_easy_cleanup);
126 LOG_ERROR(
"HttpClient: Failed to initialise CURL handle");
131 HeaderState headerState;
133 curl_easy_setopt(curl.get(), CURLOPT_URL, request.
url.c_str());
134 curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback);
135 curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response.
body);
136 curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, HeaderCallback);
137 curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headerState);
138 curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, request.
userAgent.c_str());
139 curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, request.
followRedirects ? 1L : 0L);
140 curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT,
static_cast<long>(request.
timeout.count()));
141 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L);
142 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L);
146 curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L);
147 curl_easy_setopt(curl.get(), CURLOPT_XFERINFOFUNCTION, XferInfoCallback);
148 const std::stop_token xferStop = stopToken;
149 curl_easy_setopt(curl.get(), CURLOPT_XFERINFODATA, &xferStop);
151 const CURLcode res = curl_easy_perform(curl.get());
153 if (res == CURLE_ABORTED_BY_CALLBACK) {
154 response.
body.clear();
159 curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response.
httpCode);
161 if (res != CURLE_OK) {
162 LOG_ERROR(
"HttpClient: Request failed: {}", curl_easy_strerror(res));
163 response.
body.clear();
168 if (response.
httpCode == 403 && headerState.rateLimited) {
169 response.
body.clear();
176 response.
body.clear();