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 }
43 }
44
46 static size_t write_http_response_body(char* data, size_t size, size_t nmemb, void* userdata) {
47 size_t total_size = size * nmemb;
48 auto* handler = static_cast<HttpRequestHandler*>(userdata);
49 if (handler) return handler->write_response_body(data, total_size);
50 return total_size;
51 }
52
54 static size_t parse_http_response_header(char* buffer, size_t size, size_t nitems, void* userdata) {
55 size_t buffer_size = size * nitems;
56 auto* headers = static_cast<Headers*>(userdata);
57 std::string key, value;
58 utils::parse_http_header_pair(buffer, buffer_size, key, value);
59 if (!key.empty()) {
60 headers->emplace(key, value);
61 }
62 return buffer_size;
63 }
64
66 bool handle_curl_message(CURLMsg* message) {
67 if (!m_response) return true;
68
69 m_response->error_message = std::string(m_error_buffer, std::strlen(m_error_buffer));
70 // m_response->error_code = utils::make_error_code(message->data.result);
71 curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &m_response->status_code);
72
73 // If a timeout occurred, override the HTTP response code.
74 if (message->data.result == CURLE_OPERATION_TIMEDOUT) {
75 m_response->status_code = 499; // Client Closed Request
76 }
77
78 if (m_response->status_code == 0 &&
79 message->data.result != CURLE_OK) {
80 m_response->status_code = 451; // Unavailable For Legal Reasons
81 }
82
83 if (message->data.result != CURLE_OK) {
84 m_response->error_code = utils::make_error_code(message->data.result);
85 } else
86 if (m_response->status_code >= 400) {
87 m_response->error_code = utils::make_http_error(m_response->status_code);
88 } else {
89 m_response->error_code = {};
90 }
91
92 const auto& valid_statuses = m_request_context->request->valid_statuses;
93 long retry_attempts = m_request_context->request->retry_attempts;
94 long& retry_attempt = m_request_context->retry_attempt;
95 ++retry_attempt;
96
97 m_response->retry_attempt = retry_attempt;
98 const bool has_curl_error = message->data.result != CURLE_OK;
99 const bool has_valid_status = valid_statuses.count(m_response->status_code) > 0;
100 const bool should_retry =
101 retry_attempts &&
102 (has_curl_error || !has_valid_status) &&
103 retry_attempt < retry_attempts &&
105
106 if (!should_retry) {
108 m_response->ready = true;
109 m_request_context->callback(std::move(m_response));
110 m_callback_called = true;
111 return true;
112 }
113 m_request_context->start_time = std::chrono::steady_clock::now();
114 m_request_context->callback(std::move(m_response));
115 m_callback_called = true;
116 return false;
117 }
118
121 CURL* get_curl() noexcept { return m_curl; }
122
125 std::unique_ptr<HttpRequestContext> get_request_context() { return std::move(m_request_context); }
126
129 uint64_t get_request_id() { return m_request_context ? m_request_context->request->request_id : 0; }
130
132 void cancel() {
133 if (!m_callback_called) {
135 m_response->status_code = 499; // Client closed request
136 m_response->ready = true;
137 if (m_request_context) {
138 m_request_context->callback(std::move(m_response));
139 }
140 m_callback_called = true;
141 }
142 }
143
144 private:
145 std::unique_ptr<HttpRequestContext> m_request_context;
146 std::unique_ptr<HttpResponse> m_response;
147 CURL* m_curl = nullptr;
148 struct curl_slist* m_headers = nullptr;
149 char m_error_buffer[CURL_ERROR_SIZE];
150 bool m_callback_called = false;
151 bool m_has_stream_chunk = false;
152 mutable std::string m_ca_file;
153
155 void init_curl() {
156 if (!m_request_context) return;
157 const auto& request = m_request_context->request;
158 m_curl = curl_easy_init();
159 if (!m_curl) return;
160
161 curl_easy_setopt(m_curl, CURLOPT_URL, request->url.c_str());
162 curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L); // Disable signals for thread safety.
163 curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, request->method.c_str());
164 if (request->head_only) {
165 curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
166 }
167 curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
168
169 set_ssl_options(*request);
170 set_request_options(*request);
171
172 curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_error_buffer);
173 curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write_http_response_body);
174 curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, this);
175 curl_easy_setopt(m_curl, CURLOPT_HEADERDATA, &m_response->headers);
176 curl_easy_setopt(m_curl, CURLOPT_HEADERFUNCTION, parse_http_response_header);
177 curl_easy_setopt(m_curl, CURLOPT_PRIVATE, this);
178 }
179
181 size_t write_response_body(const char* data, size_t total_size) {
182 if (m_response) {
183 m_response->content.append(data, total_size);
184 }
185 emit_stream_chunk(data, total_size);
186 return total_size;
187 }
188
190 void emit_stream_chunk(const char* data, size_t total_size) {
191 if (!data || total_size == 0 || !m_request_context ||
192 !m_request_context->request ||
193 !m_request_context->request->streaming ||
194 !m_request_context->callback) {
195 return;
196 }
197
198# if __cplusplus >= 201402L
199 auto chunk = std::make_unique<HttpResponse>();
200# else
201 auto chunk = std::unique_ptr<HttpResponse>(new HttpResponse());
202# endif
203
204 if (m_response) {
205 chunk->headers = m_response->headers;
206 }
207 chunk->content.assign(data, total_size);
208 chunk->retry_attempt = m_request_context->retry_attempt + 1;
209 chunk->ready = false;
210 chunk->stream_chunk = true;
211 if (m_curl) {
212 curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &chunk->status_code);
213 }
214
215 m_has_stream_chunk = true;
216
217 try {
218 m_request_context->callback(std::move(chunk));
219 } catch (const std::exception& e) {
220 KURLYK_HANDLE_ERROR(e, "Unhandled exception in HttpRequestHandler streaming callback");
221 } catch (...) {
222 // Unknown fatal error in streaming callback
223 }
224 }
225
227 curl_easy_getinfo(m_curl, CURLINFO_NAMELOOKUP_TIME, &m_response->namelookup_time);
228 curl_easy_getinfo(m_curl, CURLINFO_CONNECT_TIME, &m_response->connect_time);
229 curl_easy_getinfo(m_curl, CURLINFO_APPCONNECT_TIME, &m_response->appconnect_time);
230 curl_easy_getinfo(m_curl, CURLINFO_PRETRANSFER_TIME, &m_response->pretransfer_time);
231 curl_easy_getinfo(m_curl, CURLINFO_STARTTRANSFER_TIME, &m_response->starttransfer_time);
232 curl_easy_getinfo(m_curl, CURLINFO_TOTAL_TIME, &m_response->total_time);
233 }
234
236 void set_ssl_options(const HttpRequest& request) {
237 if (!request.cert_file.empty()) {
238 curl_easy_setopt(m_curl, CURLOPT_SSLCERT, request.cert_file.c_str());
239 }
240 if (!request.key_file.empty()) {
241 curl_easy_setopt(m_curl, CURLOPT_SSLKEY, request.key_file.c_str());
242 }
243 if (!request.ca_file.empty()) {
244 curl_easy_setopt(m_curl, CURLOPT_CAINFO, request.ca_file.c_str());
245 } else {
246 curl_easy_setopt(m_curl, CURLOPT_CAINFO, get_ca_file_path());
247 }
248 if (!request.ca_path.empty()) {
249 curl_easy_setopt(m_curl, CURLOPT_CAPATH, request.ca_path.c_str());
250 }
251 curl_easy_setopt(m_curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_DEFAULT);
252 }
253
255 void set_request_options(const HttpRequest& request) {
256 curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, request.follow_location);
257 curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, request.max_redirects);
258 curl_easy_setopt(m_curl, CURLOPT_AUTOREFERER, request.auto_referer);
259 curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, request.timeout);
260 curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, request.connect_timeout);
261
262 set_custom_headers(request);
263 set_proxy_options(request);
264 set_cookie_options(request);
265 set_request_body(request);
266 if (request.use_interface && request.proxy_server.empty()) {
267 curl_easy_setopt(m_curl, CURLOPT_INTERFACE, request.interface_name.c_str());
268 }
269 curl_easy_setopt(m_curl, CURLOPT_VERBOSE, request.verbose);
270 curl_easy_setopt(m_curl, CURLOPT_HEADER, request.debug_header);
271 }
272
274 void set_custom_headers(const HttpRequest& request) {
275 if (!request.user_agent.empty() && request.headers.count("User-Agent") == 0) {
276 curl_easy_setopt(m_curl, CURLOPT_USERAGENT, request.user_agent.c_str());
277 }
278 if (!request.accept_encoding.empty() && request.headers.count("Accept-Encoding") == 0) {
279 curl_easy_setopt(m_curl, CURLOPT_ACCEPT_ENCODING, request.accept_encoding.c_str());
280 }
281
282 for (const auto& header : request.headers) {
283 std::string header_line = header.first + ": " + header.second;
284 m_headers = curl_slist_append(m_headers, header_line.c_str());
285 if (!m_headers) {
286 curl_slist_free_all(m_headers);
287 break;
288 }
289 }
290 if (m_headers) {
291 curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_headers);
292 }
293 }
294
296 void set_proxy_options(const HttpRequest& request) {
297 if (!request.proxy_server.empty()) {
298 curl_easy_setopt(m_curl, CURLOPT_PROXY, request.proxy_server.c_str());
299 curl_easy_setopt(m_curl, CURLOPT_PROXYTYPE, to_curl_proxy_type(request.proxy_type));
300 curl_easy_setopt(m_curl, CURLOPT_HTTPPROXYTUNNEL, request.proxy_tunnel);
301 if (!request.proxy_auth.empty()) {
302 curl_easy_setopt(m_curl, CURLOPT_PROXYUSERPWD, request.proxy_auth.c_str());
303 curl_easy_setopt(m_curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
304 }
305 }
306 }
307
309 void set_cookie_options(const HttpRequest& request) {
310 if (!request.cookie.empty() && request.headers.count("Cookie") == 0) {
311 curl_easy_setopt(m_curl, CURLOPT_COOKIE, request.cookie.c_str());
312 } else if (!request.cookie_file.empty()) {
313 curl_easy_setopt(m_curl, CURLOPT_COOKIEFILE, request.cookie_file.c_str());
314 curl_easy_setopt(m_curl, CURLOPT_COOKIEJAR, request.cookie_file.c_str());
315 if (request.clear_cookie_file) {
316 curl_easy_setopt(m_curl, CURLOPT_COOKIELIST, "ALL");
317 }
318 }
319 }
320
322 void set_request_body(const HttpRequest& request) {
323 if (request.head_only) return;
324 if (utils::case_insensitive_equal(request.method, "POST") ||
325 utils::case_insensitive_equal(request.method, "PUT") ||
326 utils::case_insensitive_equal(request.method, "PATCH") ||
327 utils::case_insensitive_equal(request.method, "DELETE")) {
328 curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, request.content.c_str());
329 curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(request.content.size()));
330 }
331 }
332
334 const char* get_ca_file_path() const {
335# if defined(_WIN32)
336 m_ca_file = utils::get_exec_dir() + "\\curl-ca-bundle.crt";
337 m_ca_file = utils::utf8_to_ansi(m_ca_file);
338# else
339 m_ca_file = utils::get_exec_dir() + "/curl-ca-bundle.crt";
340# endif
341 return m_ca_file.c_str();
342 }
343 }; // HttpRequestHandler
344
345} // namespace kurlyk
346
347#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 m_has_stream_chunk
Indicates if a streaming body chunk was emitted.
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.
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.
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.
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, sends the request without a 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 the response received from an HTTP request, including headers, content,...
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.