Skip to content

Commit 93d87e2

Browse files
committed
Implemented CRTP for HttpRequests with body (POST/PUT), this way we can determine it at compile time ;) and there is no way we can do a .body(...) when we issue a get or a head
1 parent d19be47 commit 93d87e2

File tree

3 files changed

+132
-13
lines changed

3 files changed

+132
-13
lines changed

src/s3cpp/httpclient.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ HttpResponse HttpRequest::execute() {
1818
}
1919
}
2020

21+
HttpResponse HttpBodyRequest::execute() {
22+
switch (this->http_method_) {
23+
case HttpMethod::Post:
24+
return client_.execute_post(*this);
25+
default:
26+
throw std::runtime_error(std::format("No matching enum Http Method"));
27+
}
28+
}
29+
2130
HttpResponse HttpClient::execute_get(HttpRequest &request) {
2231
if (!curl_handle) {
2332
throw std::runtime_error(
@@ -112,6 +121,63 @@ HttpResponse HttpClient::execute_head(HttpRequest &request) {
112121
return HttpResponse(static_cast<int>(response_code), std::move(headers_buf));
113122
}
114123

124+
HttpResponse HttpClient::execute_post(HttpBodyRequest &request) {
125+
if (!curl_handle) {
126+
throw std::runtime_error(
127+
// this can happen both when cURL handle is not initialized or when it
128+
// is invalidated in the HTTPClient copy constructor
129+
"cURL handle is invalid");
130+
}
131+
std::string body_buf;
132+
std::map<std::string, std::string> headers_buf;
133+
std::string error_buf;
134+
135+
// TODO(cristian): from libcurl docs, they state that each curl handle has
136+
// "sticky" params, this is why we are resetting at each get request
137+
// However, I think we should only do this when moving a new handle the only
138+
// thing that will change is the URL from now
139+
//
140+
// curl_easy_reset(curl_handle);
141+
142+
curl_easy_setopt(curl_handle, CURLOPT_URL, request.getURL().c_str());
143+
// body callback
144+
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_callback);
145+
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &body_buf);
146+
// headers callback
147+
curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, header_callback);
148+
curl_easy_setopt(curl_handle, CURLOPT_HEADERDATA, &headers_buf);
149+
// post/put body
150+
curl_easy_setopt(curl_handle, CURLOPT_POST,
151+
1L); // sets CURLOPT_NOBODY CURLOPT_HTTPGET to 0
152+
curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, request.getBody().c_str());
153+
154+
curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, request.getTimeout());
155+
156+
// merge client and request headers
157+
// https://stackoverflow.com/questions/34321719
158+
auto headers = request.getHeaders();
159+
headers.insert(this->getHeaders().begin(), this->getHeaders().end());
160+
struct curl_slist *list = NULL;
161+
for (const auto &[k, v] : headers) {
162+
list = curl_slist_append(list, std::format("{}: {}", k, v).c_str());
163+
}
164+
curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, list);
165+
166+
CURLcode code = curl_easy_perform(curl_handle);
167+
curl_slist_free_all(list);
168+
if (code != CURLE_OK) {
169+
throw std::runtime_error(
170+
std::format("libcurl error for request: {}", curl_easy_strerror(code)));
171+
}
172+
173+
// get HTTP code
174+
long response_code = 0;
175+
curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CODE, &response_code);
176+
177+
return HttpResponse(static_cast<int>(response_code), std::move(body_buf),
178+
std::move(headers_buf));
179+
}
180+
115181
size_t HttpClient::write_callback(char *ptr, size_t size, size_t nmemb,
116182
void *userdata) {
117183
std::string *buffer = static_cast<std::string *>(userdata);

src/s3cpp/httpclient.h

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,46 +47,83 @@ class HttpResponse {
4747
};
4848

4949
// HttpRequest will handle all the headers and request params
50-
class HttpRequest {
50+
//
51+
// Curiously Recurring Template Pattern (CRTP)
52+
//
53+
// POST/PUT must a have a body member variable that GET/HEAD don't
54+
// we want to do this at compile-time, however, because we are using a fluent
55+
// builder pattern, if we were to do regular inheritance, when using .body() we
56+
// would return a `&HttpBodyRequest : public HttpRequest`, which would not allow
57+
// us to chain useful methods on the HttpRequest class such as .timeout()
58+
59+
template <typename T> class HttpRequestBase {
5160
public:
52-
HttpRequest(HttpClient &client, std::string URL,
53-
const HttpMethod &http_method)
61+
HttpRequestBase(HttpClient &client, std::string URL,
62+
const HttpMethod &http_method)
5463
: client_(client), URL_(std::move(URL)),
5564
http_method_(std::move(http_method)), timeout_(0) {};
5665

57-
HttpRequest &timeout(const long long &seconds) {
66+
T &timeout(const long long &seconds) {
5867
timeout_ = std::chrono::seconds(seconds);
59-
return *this;
68+
return static_cast<T &>(*this);
6069
}
61-
HttpRequest &timeout(const std::chrono::seconds &seconds) {
70+
T &timeout(const std::chrono::seconds &seconds) {
6271
timeout_ = seconds;
63-
return *this;
72+
return static_cast<T &>(*this);
6473
}
65-
HttpRequest &header(const std::string &header_, const std::string &value) {
74+
T &header(const std::string &header_, const std::string &value) {
6675
headers_[header_] = value;
67-
return *this;
76+
return static_cast<T &>(*this);
6877
}
6978

70-
HttpResponse execute();
71-
7279
const std::string &getURL() const { return URL_; }
7380
const long long getTimeout() const { return timeout_.count(); }
7481
const std::unordered_map<std::string, std::string> &getHeaders() const {
7582
return headers_;
7683
}
7784

78-
private:
85+
protected:
7986
HttpClient &client_;
8087
std::string URL_;
8188
std::unordered_map<std::string, std::string> headers_;
8289
std::chrono::seconds timeout_;
8390
HttpMethod http_method_;
8491
};
8592

93+
// GET/HEAD
94+
class HttpRequest : public HttpRequestBase<HttpRequest> {
95+
public:
96+
using HttpRequestBase::HttpRequestBase;
97+
HttpResponse execute();
98+
};
99+
100+
// POST/PUT
101+
class HttpBodyRequest : public HttpRequestBase<HttpBodyRequest> {
102+
public:
103+
HttpBodyRequest(HttpClient &client, std::string URL,
104+
const HttpMethod &http_method)
105+
: HttpRequestBase(client, std::move(URL), http_method) {}
106+
107+
HttpBodyRequest &body(const std::string &data) {
108+
body_ = std::move(data);
109+
return (*this);
110+
}
111+
112+
const std::string &getBody() const { return body_; }
113+
114+
HttpResponse execute();
115+
116+
private:
117+
std::string body_;
118+
};
119+
86120
// HttpClient should only focus on handling the cURL handle
87121
// and making the request (HttpRequest) and returning HttpResponse
88122
class HttpClient {
89-
friend class HttpRequest; // `execute()` is invoked from the request only
123+
// `execute()` is invoked from the request only
124+
friend class HttpRequest;
125+
friend class HttpBodyRequest;
126+
90127
public:
91128
HttpClient() {
92129
curl_handle = curl_easy_init();
@@ -131,6 +168,9 @@ class HttpClient {
131168
[[nodiscard]] HttpRequest head(const std::string &URL) {
132169
return HttpRequest{*this, URL, HttpMethod::Head};
133170
};
171+
[[nodiscard]] HttpBodyRequest post(const std::string &URL) {
172+
return HttpBodyRequest{*this, URL, HttpMethod::Post};
173+
};
134174

135175
private:
136176
CURL *curl_handle = nullptr;
@@ -146,6 +186,7 @@ class HttpClient {
146186
// this is invoked by HttpRequest
147187
HttpResponse execute_get(HttpRequest &request);
148188
HttpResponse execute_head(HttpRequest &request);
189+
HttpResponse execute_post(HttpBodyRequest &request);
149190

150191
const std::unordered_map<std::string, std::string> &getHeaders() const {
151192
return headers_;

test/httpclient_test.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,15 @@ TEST(HTTP, HTTPHeaderOrder) {
198198
it2++;
199199
}
200200
}
201+
202+
TEST(HTTP, HTTPPost) {
203+
HttpClient client{};
204+
std::string data = "This is expected to be sent back as part of response body";
205+
HttpBodyRequest req = client.post("https://postman-echo.com/post").body(data);
206+
EXPECT_EQ(req.getBody(), data);
207+
HttpResponse resp = req.execute();
208+
EXPECT_TRUE(resp.is_ok());
209+
EXPECT_EQ(resp.status(), 200);
210+
EXPECT_THAT(resp.body(), testing::HasSubstr(data));
211+
}
212+

0 commit comments

Comments
 (0)