This is an automated email from the ASF dual-hosted git repository.
wangdan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pegasus.git
The following commit(s) were added to refs/heads/master by this push:
new c0fcba082 feat(http_client): add http_url class to help build URLs for
http client (#1843)
c0fcba082 is described below
commit c0fcba082cd6665625dd6c42438c33c35e19d216
Author: Dan Wang <[email protected]>
AuthorDate: Fri Jan 19 14:16:23 2024 +0800
feat(http_client): add http_url class to help build URLs for http client
(#1843)
There are multiple components for a URL used to issue http requests,
such as scheme, host, port, path and query. However, to build each
URL by such components, we could assemble them into a string only
by fmt::format or concatenating them together with some separators.
This PR is to provide a class named http_url that help build URLs on
demand. Just specify each component we need to http_url, then
extract the URL string. We could set the extracted the URL string,
or just set http_url object itself to http client to tell what is the URL
for the http requests.
---
src/http/http_client.cpp | 315 +++++++++++++++++++++++++++-----
src/http/http_client.h | 104 ++++++++++-
src/http/test/http_client_test.cpp | 355 ++++++++++++++++++++++++++++++++++---
src/utils/enum_helper.h | 3 +-
4 files changed, 704 insertions(+), 73 deletions(-)
diff --git a/src/http/http_client.cpp b/src/http/http_client.cpp
index dbd60494b..1af5ebb4e 100644
--- a/src/http/http_client.cpp
+++ b/src/http/http_client.cpp
@@ -34,6 +34,188 @@ DSN_DEFINE_uint32(http,
"The maximum time in milliseconds that you allow the libcurl
transfer operation "
"to complete");
+#define RETURN_IF_CURL_NOT_OK(expr, ok, ...)
\
+ do {
\
+ const auto &code = (expr);
\
+ if (dsn_unlikely(code != (ok))) {
\
+ std::string msg(fmt::format("{}: {}", fmt::format(__VA_ARGS__),
to_error_msg(code))); \
+ return dsn::error_s::make(to_error_code(code), msg);
\
+ }
\
+ } while (0)
+
+#define CHECK_IF_CURL_OK(expr, ok, ...)
\
+ do {
\
+ const auto &code = (expr);
\
+ CHECK_EXPRESSION(
\
+ #expr == #ok, code == (ok), "{}: {}", fmt::format(__VA_ARGS__),
to_error_msg(code)); \
+ } while (0)
+
+#define CHECK_IF_CURL_URL_OK(url, expr, ...)
\
+ do {
\
+ CHECK_NOTNULL(url, "CURLU object has not been allocated");
\
+ CHECK_IF_CURL_OK(expr, CURLUE_OK, __VA_ARGS__);
\
+ } while (0)
+
+#define CHECK_IF_CURL_URL_SET_OK(url, part, content, ...)
\
+ CHECK_IF_CURL_URL_OK(url, curl_url_set(url, CURLUPART_##part, content, 0),
__VA_ARGS__)
+
+#define CHECK_IF_CURL_URL_SET_NULL_OK(url, part, ...)
\
+ CHECK_IF_CURL_URL_SET_OK(url,
\
+ part,
\
+ nullptr,
\
+ "curl_url_set(part = " #part
\
+ ", content = nullptr) should always return
CURLUE_OK")
+
+// Set "http" as the default scheme.
+#define SET_DEFAULT_HTTP_SCHEME(url)
\
+ CHECK_IF_CURL_URL_SET_OK(url,
\
+ SCHEME,
\
+ enum_to_val(http_scheme::kHttp,
std::string()).c_str(), \
+ "failed to set CURLUPART_SCHEME with 'http'")
+
+namespace {
+
+inline dsn::error_code to_error_code(CURLUcode code)
+{
+ switch (code) {
+ case CURLUE_OK:
+ return dsn::ERR_OK;
+ default:
+ return dsn::ERR_CURL_FAILED;
+ }
+}
+
+inline std::string to_error_msg(CURLUcode code)
+{
+ return fmt::format("code={}, desc=\"{}\"", static_cast<int>(code),
curl_url_strerror(code));
+}
+
+CURLU *new_curlu()
+{
+ CURLU *url = curl_url();
+ CHECK_NOTNULL(url, "fail to allocate a CURLU object due to out of memory");
+
+ SET_DEFAULT_HTTP_SCHEME(url);
+
+ return url;
+}
+
+CURLU *dup_curlu(const CURLU *url)
+{
+ CURLU *new_url = curl_url_dup(url);
+ CHECK_NOTNULL(new_url, "fail to duplicate a CURLU object due to out of
memory");
+
+ return new_url;
+}
+
+} // anonymous namespace
+
+void http_url::curlu_deleter::operator()(CURLU *url) const
+{
+ if (url == nullptr) {
+ return;
+ }
+
+ curl_url_cleanup(url);
+ url = nullptr;
+}
+
+http_url::http_url() noexcept : _url(new_curlu(), curlu_deleter()) {}
+
+http_url::http_url(const http_url &rhs) noexcept :
_url(dup_curlu(rhs._url.get()), curlu_deleter())
+{
+}
+
+http_url &http_url::operator=(const http_url &rhs) noexcept
+{
+ if (this == &rhs) {
+ return *this;
+ }
+
+ // The existing deleter will not be cleared.
+ _url.reset(dup_curlu(rhs._url.get()));
+ return *this;
+}
+
+http_url::http_url(http_url &&rhs) noexcept : _url(std::move(rhs._url)) {}
+
+http_url &http_url::operator=(http_url &&rhs) noexcept
+{
+ if (this == &rhs) {
+ return *this;
+ }
+
+ _url = std::move(rhs._url);
+ return *this;
+}
+
+void http_url::clear()
+{
+ // Setting the url with nullptr would lead to the release of memory for
each part of the url,
+ // thus clearing the url.
+ CHECK_IF_CURL_URL_SET_NULL_OK(_url.get(), URL);
+
+ SET_DEFAULT_HTTP_SCHEME(_url.get());
+}
+
+#define RETURN_IF_CURL_URL_NOT_OK(expr, ...) RETURN_IF_CURL_NOT_OK(expr,
CURLUE_OK, __VA_ARGS__)
+
+#define RETURN_IF_CURL_URL_SET_NOT_OK(part, content, flags)
\
+ RETURN_IF_CURL_URL_NOT_OK(
\
+ curl_url_set(_url.get(), part, content, flags), "failed to set " #part
" to {}", content)
+
+#define RETURN_IF_CURL_URL_GET_NOT_OK(part, content_pointer, flags)
\
+ RETURN_IF_CURL_URL_NOT_OK(curl_url_get(_url.get(), part, content_pointer,
flags), \
+ "failed to get " #part)
+
+#define DEF_HTTP_URL_SET_FUNC(name, part)
\
+ dsn::error_s http_url::set_##name(const char *content)
\
+ {
\
+ RETURN_IF_CURL_URL_SET_NOT_OK(CURLUPART_##part, content, 0);
\
+
\
+ return dsn::error_s::ok();
\
+ }
+
+DEF_HTTP_URL_SET_FUNC(url, URL)
+
+DEF_HTTP_URL_SET_FUNC(scheme, SCHEME)
+
+dsn::error_s http_url::set_scheme(http_scheme scheme)
+{
+ return set_scheme(enum_to_val(scheme, std::string()).c_str());
+}
+
+DEF_HTTP_URL_SET_FUNC(host, HOST)
+
+DEF_HTTP_URL_SET_FUNC(port, PORT)
+
+dsn::error_s http_url::set_port(uint16_t port) { return
set_port(std::to_string(port).c_str()); }
+
+DEF_HTTP_URL_SET_FUNC(path, PATH)
+
+DEF_HTTP_URL_SET_FUNC(query, QUERY)
+
+#undef DEF_HTTP_URL_SET_FUNC
+
+dsn::error_s http_url::to_string(std::string &url) const
+{
+ CHECK_NOTNULL(_url, "CURLU object has not been allocated");
+
+ char *content;
+ RETURN_IF_CURL_URL_GET_NOT_OK(CURLUPART_URL, &content, 0);
+
+ url = content;
+
+ // Free the returned string.
+ curl_free(content);
+
+ return dsn::error_s::ok();
+}
+
+#undef RETURN_IF_CURL_URL_GET_NOT_OK
+#undef RETURN_IF_CURL_URL_SET_NOT_OK
+#undef RETURN_IF_CURL_URL_NOT_OK
+
http_client::http_client()
: _curl(nullptr),
_method(http_method::GET),
@@ -74,35 +256,46 @@ inline dsn::error_code to_error_code(CURLcode code)
} // anonymous namespace
-#define RETURN_IF_CURL_NOT_OK(expr, ...)
\
+#define RETURN_IF_CURL_EASY_NOT_OK(expr, ...)
\
do {
\
- const auto code = (expr);
\
- if (dsn_unlikely(code != CURLE_OK)) {
\
- std::string msg(fmt::format("{}: {}", fmt::format(__VA_ARGS__),
to_error_msg(code))); \
- return dsn::error_s::make(to_error_code(code), msg);
\
- }
\
+ CHECK_NOTNULL(_curl, "CURL object has not been allocated");
\
+ RETURN_IF_CURL_NOT_OK(expr, CURLE_OK, __VA_ARGS__);
\
} while (0)
-#define RETURN_IF_SETOPT_NOT_OK(opt, input)
\
- RETURN_IF_CURL_NOT_OK(curl_easy_setopt(_curl, opt, input),
\
- "failed to set " #opt " to"
\
- " " #input)
+#define RETURN_IF_CURL_EASY_SETOPT_NOT_OK(opt, input)
\
+ RETURN_IF_CURL_EASY_NOT_OK(curl_easy_setopt(_curl, CURLOPT_##opt, input),
\
+ "failed to set " #opt " with " #input)
-#define RETURN_IF_GETINFO_NOT_OK(info, output)
\
- RETURN_IF_CURL_NOT_OK(curl_easy_getinfo(_curl, info, output), "failed to
get from " #info)
+#define RETURN_IF_CURL_EASY_GETINFO_NOT_OK(info, output)
\
+ RETURN_IF_CURL_EASY_NOT_OK(curl_easy_getinfo(_curl, CURLINFO_##info,
output), \
+ "failed to get from " #info)
-#define RETURN_IF_EXEC_METHOD_NOT_OK()
\
- RETURN_IF_CURL_NOT_OK(curl_easy_perform(_curl),
\
- "failed to perform http request(method={}, url={})",
\
- enum_to_string(_method),
\
- _url)
+#define RETURN_IF_CURL_EASY_PERFORM_NOT_OK()
\
+ RETURN_IF_CURL_EASY_NOT_OK(curl_easy_perform(_curl),
\
+ "failed to perform http request(method={},
url={})", \
+ enum_to_string(_method),
\
+ _url)
+
+#define CHECK_IF_CURL_EASY_OK(expr, ...)
\
+ do {
\
+ CHECK_NOTNULL(_curl, "CURL object has not been allocated");
\
+ CHECK_IF_CURL_OK(expr, CURLE_OK, __VA_ARGS__);
\
+ } while (0)
+
+#define CHECK_IF_CURL_EASY_SETOPT_OK(opt, input, ...)
\
+ do {
\
+ CHECK_NOTNULL(_curl, "CURL object has not been allocated");
\
+ CHECK_IF_CURL_EASY_OK(curl_easy_setopt(_curl, CURLOPT_##opt, input),
\
+ "failed to set " #opt " with " #input ": {}",
\
+ fmt::format(__VA_ARGS__));
\
+ } while (0)
dsn::error_s http_client::init()
{
if (_curl == nullptr) {
_curl = curl_easy_init();
if (_curl == nullptr) {
- return dsn::error_s::make(dsn::ERR_CURL_FAILED, "fail to
initialize curl");
+ return dsn::error_s::make(dsn::ERR_CURL_FAILED, "fail to allocate
CURL object");
}
} else {
curl_easy_reset(_curl);
@@ -113,20 +306,20 @@ dsn::error_s http_client::init()
// Additional messages for errors are needed.
clear_error_buf();
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_ERRORBUFFER, _error_buf);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(ERRORBUFFER, _error_buf);
// Set with NOSIGNAL since we are multi-threaded.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_NOSIGNAL, 1L);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(NOSIGNAL, 1L);
// Redirects are supported.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_FOLLOWLOCATION, 1L);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(FOLLOWLOCATION, 1L);
// Before 8.3.0, CURLOPT_MAXREDIRS was unlimited.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_MAXREDIRS, 20);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(MAXREDIRS, 20);
// Set common timeout for transfer operation. Users could also change it
with their
// custom values by `set_timeout`.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_TIMEOUT_MS,
static_cast<long>(FLAGS_curl_timeout_ms));
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(TIMEOUT_MS,
static_cast<long>(FLAGS_curl_timeout_ms));
// A lambda can only be converted to a function pointer if it does not
capture:
//
https://stackoverflow.com/questions/28746744/passing-capturing-lambda-as-function-pointer
@@ -135,10 +328,10 @@ dsn::error_s http_client::init()
http_client *client = reinterpret_cast<http_client *>(param);
return client->on_response_data(buffer, size * nmemb);
};
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_WRITEFUNCTION, callback);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(WRITEFUNCTION, callback);
// This http_client object itself is passed to the callback function.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_WRITEDATA, reinterpret_cast<void *>(this));
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(WRITEDATA, reinterpret_cast<void
*>(this));
return dsn::error_s::ok();
}
@@ -182,21 +375,50 @@ size_t http_client::on_response_data(const void *data,
size_t length)
return (*_recv_callback)(data, length) ? length :
std::numeric_limits<size_t>::max();
}
-dsn::error_s http_client::set_url(const std::string &url)
+#define CHECK_IF_CURL_EASY_SETOPT_CURLU_OK()
\
+ CHECK_IF_CURL_EASY_SETOPT_OK(
\
+ CURLU,
\
+ _url.curlu(),
\
+ "once CURLOPT_CURLU is supported, curl_easy_setopt should always "
\
+ "return CURLE_OK, see https://curl.se/libcurl/c/CURLOPT_CURLU.html")
+
+dsn::error_s http_client::set_url(const std::string &new_url)
{
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_URL, url.c_str());
+ // DO NOT call curl_easy_setopt() on CURLOPT_URL, since the CURLOPT_URL
string is ignored
+ // if CURLOPT_CURLU is set. See following docs for details:
+ // * https://curl.se/libcurl/c/CURLOPT_CURLU.html
+ // * https://curl.se/libcurl/c/CURLOPT_URL.html
+ //
+ // Use a temporary object for the reason that once the error occurred,
`_url` would not
+ // be dirty.
+ http_url tmp;
+ RETURN_NOT_OK(tmp.set_url(new_url.c_str()));
- _url = url;
+ set_url(std::move(tmp));
return dsn::error_s::ok();
}
+void http_client::set_url(const http_url &new_url)
+{
+ _url = new_url;
+ CHECK_IF_CURL_EASY_SETOPT_CURLU_OK();
+}
+
+void http_client::set_url(http_url &&new_url)
+{
+ _url = std::move(new_url);
+ CHECK_IF_CURL_EASY_SETOPT_CURLU_OK();
+}
+
dsn::error_s http_client::with_post_method(const std::string &data)
{
- // No need to enable CURLOPT_POST by
`RETURN_IF_SETOPT_NOT_OK(CURLOPT_POST, 1L)`, since using
- // either of CURLOPT_POSTFIELDS or CURLOPT_COPYPOSTFIELDS implies setting
CURLOPT_POST to 1.
+ // No need to enable CURLOPT_POST by
`RETURN_IF_CURL_EASY_SETOPT_NOT_OK(POST, 1L)`,
+ // since using either of CURLOPT_POSTFIELDS or CURLOPT_COPYPOSTFIELDS
implies setting
+ // CURLOPT_POST to 1.
+ //
// See https://curl.se/libcurl/c/CURLOPT_POSTFIELDS.html for details.
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_POSTFIELDSIZE,
static_cast<long>(data.size()));
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_COPYPOSTFIELDS, data.data());
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(POSTFIELDSIZE,
static_cast<long>(data.size()));
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(COPYPOSTFIELDS, data.data());
_method = http_method::POST;
return dsn::error_s::ok();
}
@@ -209,7 +431,7 @@ dsn::error_s http_client::set_method(http_method method)
// `with_post_method`.
switch (method) {
case http_method::GET:
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPGET, 1L);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPGET, 1L);
break;
default:
LOG_FATAL("Unsupported http_method");
@@ -219,22 +441,22 @@ dsn::error_s http_client::set_method(http_method method)
return dsn::error_s::ok();
}
-dsn::error_s http_client::set_auth(http_auth_type authType)
+dsn::error_s http_client::set_auth(http_auth_type auth_type)
{
- switch (authType) {
+ switch (auth_type) {
case http_auth_type::SPNEGO:
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_NEGOTIATE);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_NEGOTIATE);
break;
case http_auth_type::DIGEST:
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_DIGEST);
break;
case http_auth_type::BASIC:
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_BASIC);
break;
case http_auth_type::NONE:
break;
default:
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_ANY);
break;
}
@@ -243,7 +465,7 @@ dsn::error_s http_client::set_auth(http_auth_type authType)
dsn::error_s http_client::set_timeout(long timeout_ms)
{
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_TIMEOUT_MS, timeout_ms);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(TIMEOUT_MS, timeout_ms);
return dsn::error_s::ok();
}
@@ -299,7 +521,7 @@ dsn::error_s http_client::process_header()
// This would work well even if `_header_list` is NULL pointer. Pass a
NULL to this option
// to reset back to no custom headers.
(https://curl.se/libcurl/c/CURLOPT_HTTPHEADER.html)
- RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPHEADER, _header_list);
+ RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPHEADER, _header_list);
// New header has been built successfully, thus mark it unchanged.
_header_changed = false;
@@ -315,7 +537,7 @@ dsn::error_s http_client::exec_method(const
http_client::recv_callback &callback
RETURN_NOT_OK(process_header());
- RETURN_IF_EXEC_METHOD_NOT_OK();
+ RETURN_IF_CURL_EASY_PERFORM_NOT_OK();
return dsn::error_s::ok();
}
@@ -336,13 +558,16 @@ dsn::error_s http_client::exec_method(std::string
*response)
dsn::error_s http_client::get_http_status(http_status_code &status_code) const
{
long response_code;
- RETURN_IF_GETINFO_NOT_OK(CURLINFO_RESPONSE_CODE, &response_code);
+ RETURN_IF_CURL_EASY_GETINFO_NOT_OK(RESPONSE_CODE, &response_code);
status_code = enum_from_val(response_code, http_status_code::kInvalidCode);
return dsn::error_s::ok();
}
-#undef RETURN_IF_EXEC_METHOD_NOT_OK
-#undef RETURN_IF_SETOPT_NOT_OK
+#undef RETURN_IF_CURL_EASY_PERFORM_NOT_OK
+#undef RETURN_IF_CURL_EASY_GETINFO_NOT_OK
+#undef RETURN_IF_CURL_EASY_SETOPT_NOT_OK
+#undef RETURN_IF_CURL_EASY_NOT_OK
+
#undef RETURN_IF_CURL_NOT_OK
} // namespace dsn
diff --git a/src/http/http_client.h b/src/http/http_client.h
index e5a36802d..6596548e8 100644
--- a/src/http/http_client.h
+++ b/src/http/http_client.h
@@ -19,18 +19,101 @@
#include <curl/curl.h>
#include <stddef.h>
+#include <stdint.h>
#include <functional>
+#include <iosfwd>
+#include <memory>
#include <string>
#include <unordered_map>
+#include "absl/strings/string_view.h"
#include "http/http_method.h"
#include "http/http_status_code.h"
+#include "utils/enum_helper.h"
#include "utils/errors.h"
+#include "utils/fmt_utils.h"
#include "utils/ports.h"
-#include "absl/strings/string_view.h"
+
+USER_DEFINED_ENUM_FORMATTER(CURLUcode)
+USER_DEFINED_ENUM_FORMATTER(CURLcode)
namespace dsn {
+// Now https and ftp have not been supported. To support them, build curl by
+// "./configure --with-ssl=... --enable-ftp ...".
+#define ENUM_FOREACH_HTTP_SCHEME(DEF)
\
+ DEF(Http, "http", http_scheme)
\
+ DEF(Https, "https", http_scheme)
\
+ DEF(Ftp, "ftp", http_scheme)
+
+enum class http_scheme : size_t
+{
+ ENUM_FOREACH_HTTP_SCHEME(ENUM_CONST_DEF) kCount,
+ kInvalidScheme,
+};
+
+ENUM_CONST_DEF_FROM_VAL_FUNC(std::string, http_scheme,
ENUM_FOREACH_HTTP_SCHEME)
+ENUM_CONST_DEF_TO_VAL_FUNC(std::string, http_scheme, ENUM_FOREACH_HTTP_SCHEME)
+
+// A class that helps http client build URLs, based on CURLU object of libcurl.
+// About CURLU object, please see: https://curl.se/libcurl/c/libcurl-url.html.
+// About the usage, please see the comments for `http_client`.
+class http_url
+{
+public:
+ // The scheme is set to `http` as default for the URL.
+ http_url() noexcept;
+
+ ~http_url() = default;
+
+ http_url(const http_url &) noexcept;
+ http_url &operator=(const http_url &) noexcept;
+
+ http_url(http_url &&) noexcept;
+ http_url &operator=(http_url &&) noexcept;
+
+ // Clear the URL. And the scheme is reset to `http`.
+ void clear();
+
+ // Operations that update the components of a URL.
+ dsn::error_s set_url(const char *url);
+ dsn::error_s set_scheme(const char *scheme);
+ dsn::error_s set_scheme(http_scheme scheme);
+ dsn::error_s set_host(const char *host);
+ dsn::error_s set_port(const char *port);
+ dsn::error_s set_port(uint16_t port);
+ dsn::error_s set_path(const char *path);
+ dsn::error_s set_query(const char *query);
+
+ // Extract the URL string.
+ dsn::error_s to_string(std::string &url) const;
+
+ // Formatter for fmt::format.
+ friend std::ostream &operator<<(std::ostream &os, const http_url &url)
+ {
+ std::string str;
+ const auto &err = url.to_string(str);
+ if (dsn_unlikely(!err)) {
+ return os << err;
+ }
+ return os << str;
+ }
+
+private:
+ friend class http_client;
+ friend void test_after_move(http_url &, http_url &);
+
+ // Only used by `http_client` to get the underlying CURLU object.
+ CURLU *curlu() const { return _url.get(); }
+
+ struct curlu_deleter
+ {
+ void operator()(CURLU *url) const;
+ };
+
+ std::unique_ptr<CURLU, curlu_deleter> _url;
+};
+
// A library for http client that provides convenient APIs to access http
services, implemented
// based on libcurl (https://curl.se/libcurl/c/).
//
@@ -47,6 +130,13 @@ namespace dsn {
// Specify the target url that you would request for:
// err = client.set_url("http://<ip>:<port>/your/path");
//
+// Or, you could use `http_url` to manage your URLs, and attach it to http
client:
+// http_url url;
+// url_err = url.set_host(host);
+// url_err = url.set_port(port);
+// url_err = url.set_path(path);
+// err = client.set_url(url); // Or err = client.set_url(std::move(url));
+//
// If you would use GET method, call `with_get_method`:
// err = client.with_get_method();
//
@@ -83,7 +173,11 @@ public:
dsn::error_s init();
// Specify the target url that the request would be sent for.
- dsn::error_s set_url(const std::string &url);
+ dsn::error_s set_url(const std::string &new_url);
+
+ // Specify the target url by `http_url` class.
+ void set_url(const http_url &new_url);
+ void set_url(http_url &&new_url);
// Using post method, with `data` as the payload for post body.
dsn::error_s with_post_method(const std::string &data);
@@ -95,7 +189,7 @@ public:
dsn::error_s set_timeout(long timeout_ms);
// Specify the http auth type which include NONE BASIC DIGEST SPNEGO
- dsn::error_s set_auth(http_auth_type authType);
+ dsn::error_s set_auth(http_auth_type auth_type);
// Operations for the header fields.
void clear_header_fields();
@@ -144,7 +238,7 @@ private:
CURL *_curl;
http_method _method;
- std::string _url;
+ http_url _url;
const recv_callback *_recv_callback;
char _error_buf[kErrorBufferBytes];
@@ -156,3 +250,5 @@ private:
};
} // namespace dsn
+
+USER_DEFINED_STRUCTURE_FORMATTER(::dsn::http_url);
diff --git a/src/http/test/http_client_test.cpp
b/src/http/test/http_client_test.cpp
index 0092e8dbf..95ecb9530 100644
--- a/src/http/test/http_client_test.cpp
+++ b/src/http/test/http_client_test.cpp
@@ -16,15 +16,17 @@
// under the License.
#include <fmt/core.h>
+#include <stdint.h>
// IWYU pragma: no_include <gtest/gtest-message.h>
// IWYU pragma: no_include <gtest/gtest-param-test.h>
// IWYU pragma: no_include <gtest/gtest-test-part.h>
#include <cstring>
#include <iostream>
#include <string>
-#include <tuple>
+#include <utility>
#include <vector>
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "http/http_client.h"
#include "http/http_method.h"
@@ -32,8 +34,12 @@
#include "utils/error_code.h"
#include "utils/errors.h"
#include "utils/fmt_logging.h"
+#include "utils/strings.h"
#include "utils/test_macros.h"
+// IWYU pragma: no_forward_declare
dsn::HttpClientMethodTest_ExecMethodByCopyUrlClass_Test
+// IWYU pragma: no_forward_declare
dsn::HttpClientMethodTest_ExecMethodByMoveUrlClass_Test
+// IWYU pragma: no_forward_declare
dsn::HttpClientMethodTest_ExecMethodByUrlString_Test
namespace dsn {
void check_expected_description_prefix(const std::string
&expected_description_prefix,
@@ -42,11 +48,263 @@ void check_expected_description_prefix(const std::string
&expected_description_p
const std::string actual_description(err.description());
std::cout << actual_description << std::endl;
- ASSERT_LT(expected_description_prefix.size(), actual_description.size());
+ ASSERT_LE(expected_description_prefix.size(), actual_description.size());
EXPECT_EQ(expected_description_prefix,
actual_description.substr(0,
expected_description_prefix.size()));
}
+const std::string kTestUrlA("http://192.168.1.2/test/api0?key0=val0");
+const std::string kTestUrlB("http://10.10.1.2/test/api1?key1=val1");
+const std::string kTestUrlC("http://172.16.1.2/test/api2?key2=val2");
+
+void set_url_string(http_url &url, const std::string &str)
+{
+ ASSERT_TRUE(url.set_url(str.c_str()));
+}
+
+void check_url_eq(const http_url &url, const std::string &expected_str)
+{
+ std::string actual_str;
+ ASSERT_TRUE(url.to_string(actual_str));
+ EXPECT_EQ(expected_str, actual_str);
+}
+
+void init_test_url(http_url &url)
+{
+ // Set original url with kTestUrlA before copy.
+ set_url_string(url, kTestUrlA);
+ check_url_eq(url, kTestUrlA);
+}
+
+void test_after_copy(http_url &url_1, http_url &url_2)
+{
+ // Check if both urls are expected immediately after copy.
+ check_url_eq(url_1, kTestUrlA);
+ check_url_eq(url_2, kTestUrlA);
+
+ // Set both urls with values other than kTestUrlA.
+ set_url_string(url_2, kTestUrlB);
+ check_url_eq(url_1, kTestUrlA);
+ check_url_eq(url_2, kTestUrlB);
+
+ set_url_string(url_1, kTestUrlC);
+ check_url_eq(url_1, kTestUrlC);
+ check_url_eq(url_2, kTestUrlB);
+}
+
+TEST(HttpUrlTest, CallCopyConstructor)
+{
+ http_url url_1;
+ init_test_url(url_1);
+
+ http_url url_2(url_1);
+ test_after_copy(url_1, url_2);
+}
+
+TEST(HttpUrlTest, CallCopyAssignmentOperator)
+{
+ http_url url_1;
+ init_test_url(url_1);
+
+ http_url url_2;
+ url_2 = url_1;
+ test_after_copy(url_1, url_2);
+}
+
+void test_after_move(http_url &url_1, http_url &url_2)
+{
+ // Check if both urls are expected immediately after move.
+ EXPECT_THAT(url_1._url, testing::IsNull());
+ check_url_eq(url_2, kTestUrlA);
+
+ // Set another url with value other than kTestUrlA.
+ set_url_string(url_2, kTestUrlB);
+ EXPECT_THAT(url_1._url, testing::IsNull());
+ check_url_eq(url_2, kTestUrlB);
+}
+
+TEST(HttpUrlTest, CallMoveConstructor)
+{
+ http_url url_1;
+ init_test_url(url_1);
+
+ http_url url_2(std::move(url_1));
+ test_after_move(url_1, url_2);
+}
+
+TEST(HttpUrlTest, CallMoveAssignmentOperator)
+{
+ http_url url_1;
+ init_test_url(url_1);
+
+ http_url url_2;
+ url_2 = std::move(url_1);
+ test_after_move(url_1, url_2);
+}
+
+const std::string kTestHost("192.168.1.2");
+const std::string kTestHttpUrl(fmt::format("http://{}/", kTestHost));
+
+struct http_scheme_case
+{
+ http_scheme scheme;
+ error_code expected_err_code;
+ std::string expected_description_prefix;
+ std::string expected_str;
+};
+
+class HttpSchemeTest : public testing::TestWithParam<http_scheme_case>
+{
+protected:
+ void test_scheme(const http_scheme_case &scheme_case)
+ {
+ const auto &actual_err = _url.set_scheme(scheme_case.scheme);
+ ASSERT_EQ(scheme_case.expected_err_code, actual_err.code());
+ if (!actual_err) {
+
NO_FATALS(check_expected_description_prefix(scheme_case.expected_description_prefix,
+ actual_err));
+ return;
+ }
+
+ ASSERT_TRUE(_url.set_host(kTestHost.c_str()));
+
+ std::string actual_str;
+ ASSERT_TRUE(_url.to_string(actual_str));
+ EXPECT_EQ(scheme_case.expected_str, actual_str);
+ }
+
+private:
+ http_url _url;
+};
+
+TEST_P(HttpSchemeTest, SetScheme) { test_scheme(GetParam()); }
+
+const std::vector<http_scheme_case> http_scheme_tests = {
+ {http_scheme::kHttp, ERR_OK, "", kTestHttpUrl},
+ {http_scheme::kHttps,
+ ERR_CURL_FAILED,
+ "ERR_CURL_FAILED: failed to set CURLUPART_SCHEME to https: code=5,
desc=\"Unsupported URL "
+ "scheme\"",
+ ""},
+ {http_scheme::kFtp,
+ ERR_CURL_FAILED,
+ "ERR_CURL_FAILED: failed to set CURLUPART_SCHEME to ftp: code=5,
desc=\"Unsupported URL "
+ "scheme\"",
+ ""},
+};
+
+INSTANTIATE_TEST_CASE_P(HttpClientTest, HttpSchemeTest,
testing::ValuesIn(http_scheme_tests));
+
+TEST(HttpUrlTest, Clear)
+{
+ http_url url;
+ url.set_url(kTestUrlA.c_str());
+
+ std::string str;
+ ASSERT_TRUE(url.to_string(str));
+ ASSERT_EQ(kTestUrlA, str);
+
+ url.clear();
+
+ // After cleared, at least it should be set with some host; otherwise,
extracting the URL
+ // string would lead to error.
+ const auto &err = url.to_string(str);
+ ASSERT_FALSE(err);
+ const std::string expected_description_prefix(
+ "ERR_CURL_FAILED: failed to get CURLUPART_URL: code=14, desc=\"No host
part in the URL\"");
+ NO_FATALS(check_expected_description_prefix(expected_description_prefix,
err));
+
+ ASSERT_TRUE(url.set_host(kTestHost.c_str()));
+ ASSERT_TRUE(url.to_string(str));
+ ASSERT_EQ(kTestHttpUrl, str);
+}
+
+struct http_url_build_case
+{
+ const char *scheme;
+ const char *host;
+ uint16_t port;
+ const char *path;
+ const char *query;
+ const char *expected_url;
+};
+
+class HttpUrlBuildTest : public testing::TestWithParam<http_url_build_case>
+{
+protected:
+ void test_build_url(const http_url_build_case &build_case)
+ {
+ if (!utils::is_empty(build_case.scheme)) {
+ // Empty scheme will lead to error.
+ ASSERT_TRUE(_url.set_scheme(build_case.scheme));
+ }
+
+ ASSERT_TRUE(_url.set_host(build_case.host));
+ ASSERT_TRUE(_url.set_port(build_case.port));
+ ASSERT_TRUE(_url.set_path(build_case.path));
+ ASSERT_TRUE(_url.set_query(build_case.query));
+
+ std::string actual_url;
+ ASSERT_TRUE(_url.to_string(actual_url));
+ EXPECT_STREQ(build_case.expected_url, actual_url.c_str());
+ }
+
+private:
+ http_url _url;
+};
+
+TEST_P(HttpUrlBuildTest, BuildUrl) { test_build_url(GetParam()); }
+
+const std::vector<http_url_build_case> http_url_tests = {
+ // Test default scheme, specified ip, empty path and query.
+ {nullptr, "10.10.1.2", 34801, "", "", "http://10.10.1.2:34801/"},
+ // Test default scheme, specified host, empty path and query.
+ {nullptr, "www.example.com", 8080, "", "", "http://www.example.com:8080/"},
+ // Test default scheme, specified ip and path, empty query.
+ {nullptr, "10.10.1.2", 34801, "/api", "", "http://10.10.1.2:34801/api"},
+ // Test default scheme, specified host and path, empty query.
+ {nullptr, "www.example.com", 8080, "/api", "",
"http://www.example.com:8080/api"},
+ // Test default scheme, specified ip, path and query.
+ {nullptr, "10.10.1.2", 34801, "/api", "foo=bar",
"http://10.10.1.2:34801/api?foo=bar"},
+ // Test default scheme, specified ip, path and query.
+ {nullptr,
+ "www.example.com",
+ 8080,
+ "/api",
+ "foo=bar",
+ "http://www.example.com:8080/api?foo=bar"},
+ // Test default scheme, specified ip, path and query with multiple keys.
+ {nullptr,
+ "10.10.1.2",
+ 34801,
+ "/api",
+ "key1=abc&key2=123456",
+ "http://10.10.1.2:34801/api?key1=abc&key2=123456"},
+ // Test default scheme, specified ip, multi-level path and query with
multiple keys.
+ {nullptr,
+ "10.10.1.2",
+ 34801,
+ "/api/multi/level/path",
+ "key1=abc&key2=123456",
+ "http://10.10.1.2:34801/api/multi/level/path?key1=abc&key2=123456"},
+ // Test specified scheme, ip, path and query with multiple keys.
+ {"http",
+ "10.10.1.2",
+ 34801,
+ "/api",
+ "key1=abc&key2=123456",
+ "http://10.10.1.2:34801/api?key1=abc&key2=123456"},
+ // Test specified scheme, ip, multi-level path and query with multiple
keys.
+ {"http",
+ "10.10.1.2",
+ 34801,
+ "/api/multi/level/path",
+ "key1=abc&key2=123456",
+ "http://10.10.1.2:34801/api/multi/level/path?key1=abc&key2=123456"},
+};
+
+INSTANTIATE_TEST_CASE_P(HttpUrlTest, HttpUrlBuildTest,
testing::ValuesIn(http_url_tests));
+
TEST(HttpClientTest, Connect)
{
http_client client;
@@ -109,12 +367,20 @@ TEST(HttpClientTest, Callback)
NO_FATALS(check_expected_description_prefix(expected_description_prefix,
err));
}
-using http_client_method_case =
- std::tuple<const char *, http_method, const char *, http_status_code,
const char *>;
+struct http_client_method_case
+{
+ const char *host;
+ uint16_t port;
+ const char *path;
+ http_method method;
+ const char *post_data;
+ http_status_code expected_http_status;
+ const char *expected_response;
+};
class HttpClientMethodTest : public
testing::TestWithParam<http_client_method_case>
{
-public:
+protected:
void SetUp() override { ASSERT_TRUE(_client.init()); }
void test_method_with_response_string(const http_status_code
expected_http_status,
@@ -153,14 +419,11 @@ public:
EXPECT_EQ(expected_http_status, actual_http_status);
}
- void test_mothod(const std::string &url,
- const http_method method,
+ void test_mothod(const http_method method,
const std::string &post_data,
const http_status_code expected_http_status,
const std::string &expected_response)
{
- ASSERT_TRUE(_client.set_url(url));
-
switch (method) {
case http_method::GET:
ASSERT_TRUE(_client.with_get_method());
@@ -176,40 +439,86 @@ public:
test_method_with_response_callback(expected_http_status,
expected_response);
}
-private:
http_client _client;
};
-TEST_P(HttpClientMethodTest, ExecMethod)
+#define TEST_HTTP_CLIENT_EXEC_METHOD(url_setter)
\
+ do {
\
+ const auto &method_case = GetParam();
\
+ url_setter(method_case.host, method_case.port, method_case.path);
\
+ test_mothod(method_case.method,
\
+ method_case.post_data,
\
+ method_case.expected_http_status,
\
+ method_case.expected_response);
\
+ } while (0)
+
+// Test setting url by string, where set_url returns an error_s which should
be checked.
+#define SET_HTTP_CLIENT_BY_URL_STRING(host, port, path)
\
+ do {
\
+ const auto &url = fmt::format("http://{}:{}{}", host, port, path);
\
+ ASSERT_TRUE(_client.set_url(url));
\
+ } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByUrlString)
{
- const char *url;
- http_method method;
- const char *post_data;
- http_status_code expected_http_status;
- const char *expected_response;
- std::tie(url, method, post_data, expected_http_status, expected_response)
= GetParam();
+ TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_URL_STRING);
+}
- http_client _client;
- test_mothod(url, method, post_data, expected_http_status,
expected_response);
+// Test setting url by copying url object, where set_url returns nothing.
+#define SET_HTTP_CLIENT_BY_COPY_URL_OBJECT(host, port, path)
\
+ do {
\
+ http_url url;
\
+ ASSERT_TRUE(url.set_host(host));
\
+ ASSERT_TRUE(url.set_port(port));
\
+ ASSERT_TRUE(url.set_path(path));
\
+ _client.set_url(url);
\
+ } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByCopyUrlClass)
+{
+ TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_COPY_URL_OBJECT);
+}
+
+// Test setting url by moving url object, where set_url returns nothing.
+#define SET_HTTP_CLIENT_BY_MOVE_URL_OBJECT(host, port, path)
\
+ do {
\
+ http_url url;
\
+ ASSERT_TRUE(url.set_host(host));
\
+ ASSERT_TRUE(url.set_port(port));
\
+ ASSERT_TRUE(url.set_path(path));
\
+ _client.set_url(std::move(url));
\
+ } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByMoveUrlClass)
+{
+ TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_MOVE_URL_OBJECT);
}
const std::vector<http_client_method_case> http_client_method_tests = {
- {"http://127.0.0.1:20001/test/get",
+ {"127.0.0.1",
+ 20001,
+ "/test/get",
http_method::POST,
"with POST DATA",
http_status_code::kBadRequest,
"please use GET method"},
- {"http://127.0.0.1:20001/test/get",
+ {"127.0.0.1",
+ 20001,
+ "/test/get",
http_method::GET,
"",
http_status_code::kOk,
"you are using GET method"},
- {"http://127.0.0.1:20001/test/post",
+ {"127.0.0.1",
+ 20001,
+ "/test/post",
http_method::POST,
"with POST DATA",
http_status_code::kOk,
"you are using POST method with POST DATA"},
- {"http://127.0.0.1:20001/test/post",
+ {"127.0.0.1",
+ 20001,
+ "/test/post",
http_method::GET,
"",
http_status_code::kBadRequest,
diff --git a/src/utils/enum_helper.h b/src/utils/enum_helper.h
index b408f5e35..b62918c53 100644
--- a/src/utils/enum_helper.h
+++ b/src/utils/enum_helper.h
@@ -179,7 +179,8 @@ class enum_helper_xxx;
helper->register_enum(#str, enum_class::ENUM_CONST(str));
#define ENUM_CONST_DEF_MAPPER(direction, from_type, to_type, enum_foreach,
enum_def) \
- inline to_type enum_##direction##_val(from_type from_val, to_type
invalid_to_val) \
+ inline to_type enum_##direction##_val(const from_type &from_val,
\
+ const to_type &invalid_to_val)
\
{
\
static const std::unordered_map<from_type, to_type> kMap =
{enum_foreach(enum_def)}; \
\
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]