MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
HttpClient.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "HttpClient.h"
20
21#include "Logger.h"
22
23#include <curl/curl.h>
24
25#include <cctype>
26#include <memory>
27#include <string_view>
28#include <thread>
29
30namespace MetaImGUI {
31
32namespace {
33
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);
36 return size * nmemb;
37}
38
39int XferInfoCallback(void* clientp, curl_off_t /*dltotal*/, curl_off_t /*dlnow*/, curl_off_t /*ultotal*/,
40 curl_off_t /*ulnow*/) {
41 const auto* token = static_cast<const std::stop_token*>(clientp);
42 return (token != nullptr && token->stop_requested()) ? 1 : 0;
43}
44
45struct HeaderState {
46 bool rateLimited = false;
47};
48
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);
53
54 // GitHub returns "X-RateLimit-Remaining: 0" alongside HTTP 403 when limited.
55 constexpr std::string_view kKey = "x-ratelimit-remaining:";
56 if (header.size() >= kKey.size()) {
57 std::string lower;
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]))));
61 }
62 if (lower == kKey) {
63 std::string_view value = header.substr(kKey.size());
64 while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
65 value.remove_prefix(1);
66 }
67 if (!value.empty() && value.front() == '0') {
68 state->rateLimited = true;
69 }
70 }
71 }
72
73 return total;
74}
75
76} // namespace
77
78HttpResponse HttpClient::Get(const HttpRequest& request, const std::stop_token& stopToken) const {
79 HttpResponse response;
80
81 // Initial attempt + up to maxRetries retries on transient network errors.
82 const int totalAttempts = std::max(1, request.maxRetries + 1);
83 for (int attempt = 0; attempt < totalAttempts; ++attempt) {
84 if (stopToken.stop_requested()) {
86 return response;
87 }
88
89 response = PerformOnce(request, stopToken);
90
91 // Only NetworkError is retryable. RateLimited and Cancelled aren't
92 // transient; Ok needs no retry.
93 if (response.status != HttpStatus::NetworkError) {
94 return response;
95 }
96
97 if (attempt + 1 >= totalAttempts) {
98 break;
99 }
100
101 // Exponential backoff: 200ms, 400ms, 800ms, ... — but bail early if
102 // a stop is requested mid-sleep.
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()) {
107 response.status = HttpStatus::Cancelled;
108 return response;
109 }
110 std::this_thread::sleep_for(std::chrono::milliseconds(50));
111 }
112 }
113
114 return response;
115}
116
118 return Get(request, std::stop_token{});
119}
120
121HttpResponse HttpClient::PerformOnce(const HttpRequest& request, const std::stop_token& stopToken) const {
122 HttpResponse response;
123
124 const std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl(curl_easy_init(), curl_easy_cleanup);
125 if (!curl) {
126 LOG_ERROR("HttpClient: Failed to initialise CURL handle");
128 return response;
129 }
130
131 HeaderState headerState;
132
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);
143
144 // Wire up the abort-on-stop progress callback so a stop_token actually
145 // interrupts an in-flight transfer instead of waiting on CURLOPT_TIMEOUT.
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);
150
151 const CURLcode res = curl_easy_perform(curl.get());
152
153 if (res == CURLE_ABORTED_BY_CALLBACK) {
154 response.body.clear();
155 response.status = HttpStatus::Cancelled;
156 return response;
157 }
158
159 curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response.httpCode);
160
161 if (res != CURLE_OK) {
162 LOG_ERROR("HttpClient: Request failed: {}", curl_easy_strerror(res));
163 response.body.clear();
165 return response;
166 }
167
168 if (response.httpCode == 403 && headerState.rateLimited) {
169 response.body.clear();
171 return response;
172 }
173
174 if (response.httpCode >= 400) {
175 LOG_ERROR("HttpClient: HTTP {}", response.httpCode);
176 response.body.clear();
178 return response;
179 }
180
181 response.status = HttpStatus::Ok;
182 return response;
183}
184
185} // namespace MetaImGUI
bool rateLimited
#define LOG_ERROR(...)
Definition Logger.h:168
HttpResponse Get(const HttpRequest &request, const std::stop_token &stopToken) const
Issue a GET request, honouring stop_token cancellation.
@ NetworkError
curl/transport failure, or non-2xx that isn't rate-limit
@ RateLimited
403 with X-RateLimit-Remaining: 0 (GitHub-style)
@ Cancelled
stop_token::stop_requested() fired during transfer
@ Ok
2xx response, body populated
int maxRetries
Retries on transient network failure (not on rate-limit/cancel/4xx).
Definition HttpClient.h:51
std::string userAgent
Definition HttpClient.h:48
std::chrono::seconds timeout
Definition HttpClient.h:49