Kurlyk
Loading...
Searching...
No Matches
HttpRequestHandler.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef _KURLYK_HTTP_REQUEST_HANDLER_HPP_INCLUDED
3#define _KURLYK_HTTP_REQUEST_HANDLER_HPP_INCLUDED
4
7
8namespace kurlyk {
9
13 public:
14
17 explicit HttpRequestHandler(std::unique_ptr<HttpRequestContext> context)
18 : m_request_context(std::move(context)) {
19 std::fill(m_error_buffer, m_error_buffer + CURL_ERROR_SIZE, '\0');
20# if __cplusplus >= 201402L
21 m_response = std::make_unique<HttpResponse>();
22# else
23 m_response = std::unique_ptr<HttpResponse>(new HttpResponse());
24# endif
25 init_curl();
26 }
27
33 if (m_curl) {
34 curl_easy_cleanup(m_curl);
35 curl_slist_free_all(m_headers);
36 }
39 m_response->status_code = 499; // Client closed request
40 m_response->ready = true;
41 m_request_context->callback(std::move(m_response));
42 m_request_context->complete();
43 }
44 }
45
47 static size_t write_http_response_body(char* data, size_t size, size_t nmemb, void* userdata) {
48 size_t total_size = size * nmemb;
49 auto* handler = static_cast<HttpRequestHandler*>(userdata);
50 if (handler) return handler->write_response_body(data, total_size);
51 return total_size;
52 }
53
55 static size_t parse_http_response_header(char* buffer, size_t size, size_t nitems, void* userdata) {
56 size_t buffer_size = size * nitems;
57 auto* headers = static_cast<Headers*>(userdata);
58 std::string key, value;
59 utils::parse_http_header_pair(buffer, buffer_size, key, value);
60 if (!key.empty()) {
61 headers->emplace(key, value);
62 }
63 return buffer_size;
64 }
65
67 bool handle_curl_message(CURLMsg* message) {
68 if (!m_response) return true;
69
70 m_response->error_message = std::string(m_error_buffer, std::strlen(m_error_buffer));
71 // m_response->error_code = utils::make_error_code(message->data.result);
72 curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &m_response->status_code);
73
74 // If a timeout occurred, override the HTTP response code.
75 if (message->data.result == CURLE_OPERATION_TIMEDOUT) {
76 m_response->status_code = 499; // Client Closed Request
77 }
78
79 if (m_response->status_code == 0 &&
80 message->data.result != CURLE_OK) {
81 m_response->status_code = 451; // Unavailable For Legal Reasons
82 }
83
84 if (message->data.result != CURLE_OK) {
85 m_response->error_code = utils::make_error_code(message->data.result);
86 } else
87 if (m_response->status_code >= 400) {
88 m_response->error_code = utils::make_http_error(m_response->status_code);
89 } else {
90 m_response->error_code = {};
91 }
92
93 const auto& valid_statuses = m_request_context->request->valid_statuses;
94 long retry_attempts = m_request_context->request->retry_attempts;
95 long& retry_attempt = m_request_context->retry_attempt;
96 ++retry_attempt;
97
98 m_response->retry_attempt = retry_attempt;
99 const bool has_curl_error = message->data.result != CURLE_OK;
100 const bool has_valid_status = valid_statuses.count(m_response->status_code) > 0;
101 const bool should_retry =
102 retry_attempts &&
103 (has_curl_error || !has_valid_status) &&
104 retry_attempt < retry_attempts &&
106
107 if (!should_retry) {
109 m_response->ready = true;
110 m_request_context->callback(std::move(m_response));
111 m_callback_called = true;
112 m_done = true;
113 if (m_request_context) m_request_context->complete();
114 return true;
115 }
116 m_request_context->start_time = std::chrono::steady_clock::now();
117 m_request_context->callback(std::move(m_response));
118 m_callback_called = true;
119 return false;
120 }
121
124 CURL* get_curl() noexcept { return m_curl; }
125
128 std::unique_ptr<HttpRequestContext> get_request_context() { return std::move(m_request_context); }
129
132 uint64_t get_request_id() { return m_request_context ? m_request_context->request->request_id : 0; }
133
136 uint64_t get_group_id() { return m_request_context ? m_request_context->request->group_id : 0; }
137
140 bool is_done() const noexcept { return m_done; }
141
144 void mark_done() noexcept { m_done = true; }
145
147 void cancel() {
148 if (!m_callback_called) {
150 m_response->status_code = 499; // Client closed request
151 m_response->ready = true;
152 if (m_request_context) {
153 m_request_context->callback(std::move(m_response));
154 }
155 m_callback_called = true;
156 m_done = true;
157 if (m_request_context) m_request_context->complete();
158 }
159 }
160
161 private:
162 std::unique_ptr<HttpRequestContext> m_request_context;
163 std::unique_ptr<HttpResponse> m_response;
164 CURL* m_curl = nullptr;
165 struct curl_slist* m_headers = nullptr;
166 char m_error_buffer[CURL_ERROR_SIZE];
167 bool m_callback_called = false;
168 bool m_has_stream_chunk = false;
169 bool m_done = false;
170 mutable std::string m_ca_file;
171
173 void init_curl() {
174 if (!m_request_context) return;
175 const auto& request = m_request_context->request;
176 m_curl = curl_easy_init();
177 if (!m_curl) return;
178
179 curl_easy_setopt(m_curl, CURLOPT_URL, request->url.c_str());
180 curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L); // Disable signals for thread safety.
181 curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, request->method.c_str());
182 if (request->head_only) {
183 curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
184 }
185 curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
186
187 set_ssl_options(*request);
188 set_request_options(*request);
189
190 curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_error_buffer);
191 curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write_http_response_body);
192 curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, this);
193 curl_easy_setopt(m_curl, CURLOPT_HEADERDATA, &m_response->headers);
194 curl_easy_setopt(m_curl, CURLOPT_HEADERFUNCTION, parse_http_response_header);
195 curl_easy_setopt(m_curl, CURLOPT_PRIVATE, this);
196 }
197
199 size_t write_response_body(const char* data, size_t total_size) {
200 if (m_response) {
201 m_response->content.append(data, total_size);
202 }
203 emit_stream_chunk(data, total_size);
204 return total_size;
205 }
206
208 void emit_stream_chunk(const char* data, size_t total_size) {
209 if (!data || total_size == 0 || !m_request_context ||
210 !m_request_context->request ||
211 !m_request_context->request->streaming ||
212 !m_request_context->callback) {
213 return;
214 }
215
216# if __cplusplus >= 201402L
217 auto chunk = std::make_unique<HttpResponse>();
218# else
219 auto chunk = std::unique_ptr<HttpResponse>(new HttpResponse());
220# endif
221
222 if (m_response) {
223 chunk->headers = m_response->headers;
224 }
225 chunk->content.assign(data, total_size);
226 chunk->retry_attempt = m_request_context->retry_attempt + 1;
227 chunk->ready = false;
228 chunk->stream_chunk = true;
229 if (m_curl) {
230 curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &chunk->status_code);
231 }
232
233 m_has_stream_chunk = true;
234
235 try {
236 m_request_context->callback(std::move(chunk));
237 } catch (const std::exception& e) {
238 KURLYK_HANDLE_ERROR(e, "Unhandled exception in HttpRequestHandler streaming callback");
239 } catch (...) {
240 // Unknown fatal error in streaming callback
241 }
242 }
243
245 curl_easy_getinfo(m_curl, CURLINFO_NAMELOOKUP_TIME, &m_response->namelookup_time);
246 curl_easy_getinfo(m_curl, CURLINFO_CONNECT_TIME, &m_response->connect_time);
247 curl_easy_getinfo(m_curl, CURLINFO_APPCONNECT_TIME, &m_response->appconnect_time);
248 curl_easy_getinfo(m_curl, CURLINFO_PRETRANSFER_TIME, &m_response->pretransfer_time);
249 curl_easy_getinfo(m_curl, CURLINFO_STARTTRANSFER_TIME, &m_response->starttransfer_time);
250 curl_easy_getinfo(m_curl, CURLINFO_TOTAL_TIME, &m_response->total_time);
251 }
252
254 void set_ssl_options(const HttpRequest& request) {
255 if (!request.cert_file.empty()) {
256 curl_easy_setopt(m_curl, CURLOPT_SSLCERT, request.cert_file.c_str());
257 }
258 if (!request.key_file.empty()) {
259 curl_easy_setopt(m_curl, CURLOPT_SSLKEY, request.key_file.c_str());
260 }
261 if (!request.ca_file.empty()) {
262 curl_easy_setopt(m_curl, CURLOPT_CAINFO, request.ca_file.c_str());
263 } else {
264 curl_easy_setopt(m_curl, CURLOPT_CAINFO, get_ca_file_path());
265 }
266 if (!request.ca_path.empty()) {
267 curl_easy_setopt(m_curl, CURLOPT_CAPATH, request.ca_path.c_str());
268 }
269 curl_easy_setopt(m_curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_DEFAULT);
270 }
271
273 void set_request_options(const HttpRequest& request) {
274 curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, request.follow_location);
275 curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, request.max_redirects);
276 curl_easy_setopt(m_curl, CURLOPT_AUTOREFERER, request.auto_referer);
277 curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, request.timeout);
278 curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, request.connect_timeout);
279
280 set_custom_headers(request);
281 set_proxy_options(request);
282 set_cookie_options(request);
283 set_request_body(request);
284 if (request.use_interface && request.proxy_server.empty()) {
285 curl_easy_setopt(m_curl, CURLOPT_INTERFACE, request.interface_name.c_str());
286 }
287 curl_easy_setopt(m_curl, CURLOPT_VERBOSE, request.verbose);
288 curl_easy_setopt(m_curl, CURLOPT_HEADER, request.debug_header);
289 }
290
292 void set_custom_headers(const HttpRequest& request) {
293 if (!request.user_agent.empty() && request.headers.count("User-Agent") == 0) {
294 curl_easy_setopt(m_curl, CURLOPT_USERAGENT, request.user_agent.c_str());
295 }
296 if (!request.accept_encoding.empty() && request.headers.count("Accept-Encoding") == 0) {
297 curl_easy_setopt(m_curl, CURLOPT_ACCEPT_ENCODING, request.accept_encoding.c_str());
298 }
299
300 for (const auto& header : request.headers) {
301 std::string header_line = header.first + ": " + header.second;
302 m_headers = curl_slist_append(m_headers, header_line.c_str());
303 if (!m_headers) {
304 curl_slist_free_all(m_headers);
305 break;
306 }
307 }
308 if (m_headers) {
309 curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_headers);
310 }
311 }
312
314 void set_proxy_options(const HttpRequest& request) {
315 if (!request.proxy_server.empty()) {
316 curl_easy_setopt(m_curl, CURLOPT_PROXY, request.proxy_server.c_str());
317 curl_easy_setopt(m_curl, CURLOPT_PROXYTYPE, to_curl_proxy_type(request.proxy_type));
318 curl_easy_setopt(m_curl, CURLOPT_HTTPPROXYTUNNEL, request.proxy_tunnel);
319 if (!request.proxy_auth.empty()) {
320 curl_easy_setopt(m_curl, CURLOPT_PROXYUSERPWD, request.proxy_auth.c_str());
321 curl_easy_setopt(m_curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
322 }
323 }
324 }
325
327 void set_cookie_options(const HttpRequest& request) {
328 if (!request.cookie.empty() && request.headers.count("Cookie") == 0) {
329 curl_easy_setopt(m_curl, CURLOPT_COOKIE, request.cookie.c_str());
330 } else if (!request.cookie_file.empty()) {
331 curl_easy_setopt(m_curl, CURLOPT_COOKIEFILE, request.cookie_file.c_str());
332 curl_easy_setopt(m_curl, CURLOPT_COOKIEJAR, request.cookie_file.c_str());
333 if (request.clear_cookie_file) {
334 curl_easy_setopt(m_curl, CURLOPT_COOKIELIST, "ALL");
335 }
336 }
337 }
338
340 void set_request_body(const HttpRequest& request) {
341 if (request.head_only) return;
342 if (utils::case_insensitive_equal(request.method, "POST") ||
343 utils::case_insensitive_equal(request.method, "PUT") ||
344 utils::case_insensitive_equal(request.method, "PATCH") ||
345 utils::case_insensitive_equal(request.method, "DELETE")) {
346 curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, request.content.c_str());
347 curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(request.content.size()));
348 }
349 }
350
352 const char* get_ca_file_path() const {
353# if defined(_WIN32)
354 m_ca_file = utils::get_exec_dir() + "\\curl-ca-bundle.crt";
355 m_ca_file = utils::utf8_to_ansi(m_ca_file);
356# else
357 m_ca_file = utils::get_exec_dir() + "/curl-ca-bundle.crt";
358# endif
359 return m_ca_file.c_str();
360 }
361 }; // HttpRequestHandler
362
363} // namespace kurlyk
364
365#endif // _KURLYK_HTTP_REQUEST_HANDLER_HPP_INCLUDED
#define KURLYK_HANDLE_ERROR(e, msg)
void cancel()
Marks the request as cancelled.
~HttpRequestHandler()
Destructor for HttpRequestHandler, handling cleanup of CURL and headers.
CURL * m_curl
CURL handle for the request.
bool is_done() const noexcept
Checks whether this request has already received its final callback.
bool m_has_stream_chunk
Indicates if a streaming body chunk was emitted.
bool m_done
Indicates if the request has received its final callback.
size_t write_response_body(const char *data, size_t total_size)
Stores received body data and emits a streaming chunk when enabled.
HttpRequestHandler(std::unique_ptr< HttpRequestContext > context)
Constructs an HttpRequestHandler with the specified request context.
void set_proxy_options(const HttpRequest &request)
Configures proxy options if set in the request.
static size_t write_http_response_body(char *data, size_t size, size_t nmemb, void *userdata)
Processes body data received from server and appends it to response content.
const char * get_ca_file_path() const
Gets the full path to the CA certificate file.
void set_ssl_options(const HttpRequest &request)
Sets SSL options such as cert, key, and CA file.
std::unique_ptr< HttpRequestContext > m_request_context
Context for the current request.
void set_cookie_options(const HttpRequest &request)
Sets cookie options if cookies are specified in the request.
static size_t parse_http_response_header(char *buffer, size_t size, size_t nitems, void *userdata)
Parses and stores response headers in the Headers container.
void init_curl()
Initializes CURL options for the request, setting headers, method, SSL, timeouts, and other parameter...
bool m_callback_called
Indicates if the callback was called.
bool handle_curl_message(CURLMsg *message)
Processes a CURL message and determines if a callback should be invoked.
uint64_t get_group_id()
Retrieves the group ID of the HTTP request.
std::string m_ca_file
Cached CA file path.
void emit_stream_chunk(const char *data, size_t total_size)
Invokes the response callback with an intermediate body chunk.
uint64_t get_request_id()
Retrieves the unique ID of the HTTP request.
void mark_done() noexcept
Marks the request as done without invoking a callback.
struct curl_slist * m_headers
CURL headers list.
char m_error_buffer[CURL_ERROR_SIZE]
Buffer for CURL error messages.
CURL * get_curl() noexcept
Retrieves the CURL handle associated with this request.
std::unique_ptr< HttpRequestContext > get_request_context()
Returns the unique pointer to the HttpRequestContext object.
void set_request_options(const HttpRequest &request)
Sets general options such as headers, cookies, and proxy.
void set_request_body(const HttpRequest &request)
Sets request body content for applicable HTTP methods.
void set_custom_headers(const HttpRequest &request)
Appends custom headers to the request if provided.
std::unique_ptr< HttpResponse > m_response
Response object.
Represents an HTTP request configuration.
bool proxy_tunnel
Enable proxy tunneling.
std::string user_agent
User-Agent header.
std::string proxy_auth
Proxy authentication in <username:password> format.
long timeout
Request timeout in seconds.
bool head_only
If true, does not download the response body (HEAD-like behavior).
bool follow_location
Automatically follow HTTP redirects.
bool debug_header
Include headers in debug output (CURLOPT_HEADER).
long connect_timeout
Connection timeout in seconds.
std::string cert_file
Path to the client certificate file.
bool verbose
Enable verbose output (CURLOPT_VERBOSE).
bool use_interface
Enable the specified network interface.
std::string interface_name
Network interface name to use for the request.
std::string content
Data payload for the request.
std::string cookie_file
Path to the cookie file; if empty, cookies are not saved.
std::string proxy_server
Proxy address in <ip:port> format.
std::string cookie
Cookie data as a string.
ProxyType proxy_type
Proxy type (e.g., HTTP, SOCKS5).
Headers headers
HTTP request headers.
bool auto_referer
Automatically set Referer header.
std::string accept_encoding
Accept-Encoding header.
std::string ca_file
Path to the CA certificate file.
std::string key_file
Path to the private key for the client certificate.
bool clear_cookie_file
Flag to clear the cookie file at the start of the request.
long max_redirects
Maximum allowed redirects.
std::string ca_path
Path to a directory containing CA certificates.
std::string method
HTTP request method (e.g., "GET", "POST").
Represents an HTTP response.
bool case_insensitive_equal(const std::string &str1, const std::string &str2) noexcept
Compares two strings case-insensitively.
@ AbortedDuringDestruction
Request handler was destroyed before completion, causing the request to abort.
@ CancelledByUser
Request was cancelled explicitly by the user via cancel().
void parse_http_header_pair(const char *buffer, const size_t &size, std::string &key, std::string &value)
Parses a header pair from a buffer.
std::error_code make_http_error(int status_code)
Creates an std::error_code from an HTTP status code.
std::error_code make_error_code(ClientError e)
Creates a std::error_code from a ClientError value.
std::string get_exec_dir()
Retrieves the directory of the executable file.
Primary namespace for the Kurlyk library, encompassing initialization, request management,...
utils::CaseInsensitiveMultimap Headers
Alias for HTTP headers, providing a case-insensitive unordered multimap.
Enables use of ClientError with std::error_code.