This is an automated email from the ASF dual-hosted git repository.

laiyingchun 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 0db1ff90d feat(http): implement http client based on libcurl (#1583)
0db1ff90d is described below

commit 0db1ff90d75de72aa0864892794017cedb5e4840
Author: Dan Wang <[email protected]>
AuthorDate: Thu Sep 28 15:25:20 2023 +0800

    feat(http): implement http client based on libcurl (#1583)
    
    https://github.com/apache/incubator-pegasus/issues/1582
    
    Http client is implemented based on libcurl. Both GET and POST methods
    are supported.
---
 src/http/http_call_registry.h      |  17 +-
 src/http/http_client.cpp           | 324 +++++++++++++++++++++++++++++++++++++
 src/http/http_client.h             | 154 ++++++++++++++++++
 src/http/http_message_parser.cpp   |   7 +-
 src/http/http_method.h             |  36 +++++
 src/http/http_server.cpp           |   5 +-
 src/http/http_server.h             |   7 +-
 src/http/pprof_http_service.cpp    |   3 +-
 src/http/test/CMakeLists.txt       |   6 +-
 src/http/test/config-test.ini      |  75 +++++++++
 src/http/test/http_client_test.cpp | 213 ++++++++++++++++++++++++
 src/http/test/http_server_test.cpp |  21 ++-
 src/http/test/main.cpp             | 120 ++++++++++++++
 src/utils/error_code.h             |   2 +
 src/utils/errors.h                 |   4 +-
 src/utils/metrics.cpp              |   3 +-
 src/utils/test/metrics_test.cpp    |   2 +-
 thirdparty/CMakeLists.txt          |  19 ++-
 18 files changed, 986 insertions(+), 32 deletions(-)

diff --git a/src/http/http_call_registry.h b/src/http/http_call_registry.h
index fef3b0c46..2e944463c 100644
--- a/src/http/http_call_registry.h
+++ b/src/http/http_call_registry.h
@@ -35,11 +35,11 @@ public:
     std::shared_ptr<http_call> find(const std::string &path) const
     {
         std::lock_guard<std::mutex> guard(_mu);
-        auto it = _call_map.find(path);
-        if (it == _call_map.end()) {
+        const auto &iter = _call_map.find(path);
+        if (iter == _call_map.end()) {
             return nullptr;
         }
-        return it->second;
+        return iter->second;
     }
 
     void remove(const std::string &path)
@@ -48,14 +48,19 @@ public:
         _call_map.erase(path);
     }
 
-    void add(std::unique_ptr<http_call> call_uptr)
+    void add(const std::shared_ptr<http_call> &call)
     {
-        auto call = std::shared_ptr<http_call>(call_uptr.release());
         std::lock_guard<std::mutex> guard(_mu);
-        CHECK_EQ_MSG(_call_map.count(call->path), 0, call->path);
+        CHECK_EQ_MSG(_call_map.count(call->path), 0, "{} has been added", 
call->path);
         _call_map[call->path] = call;
     }
 
+    void add(std::unique_ptr<http_call> call_uptr)
+    {
+        auto call = std::shared_ptr<http_call>(call_uptr.release());
+        add(call);
+    }
+
     std::vector<std::shared_ptr<http_call>> list_all_calls() const
     {
         std::lock_guard<std::mutex> guard(_mu);
diff --git a/src/http/http_client.cpp b/src/http/http_client.cpp
new file mode 100644
index 000000000..32ae5eaea
--- /dev/null
+++ b/src/http/http_client.cpp
@@ -0,0 +1,324 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "http/http_client.h"
+
+#include <fmt/core.h>
+#include <limits>
+#include <utility>
+
+#include "curl/curl.h"
+#include "utils/error_code.h"
+#include "utils/flags.h"
+#include "utils/fmt_logging.h"
+
+namespace dsn {
+
+DSN_DEFINE_uint32(http,
+                  curl_timeout_ms,
+                  10000,
+                  "The maximum time in milliseconds that you allow the libcurl 
transfer operation "
+                  "to complete");
+
+http_client::http_client()
+    : _curl(nullptr),
+      _method(http_method::GET),
+      _recv_callback(nullptr),
+      _header_changed(true),
+      _header_list(nullptr)
+{
+    // Since `kErrorBufferBytes` is private, `static_assert` have to be put in 
constructor.
+    static_assert(http_client::kErrorBufferBytes >= CURL_ERROR_SIZE,
+                  "The error buffer used by libcurl must be at least 
CURL_ERROR_SIZE bytes big");
+
+    clear_error_buf();
+}
+
+http_client::~http_client()
+{
+    if (_curl != nullptr) {
+        curl_easy_cleanup(_curl);
+        _curl = nullptr;
+    }
+
+    free_header_list();
+}
+
+namespace {
+
+inline dsn::error_code to_error_code(CURLcode code)
+{
+    switch (code) {
+    case CURLE_OK:
+        return dsn::ERR_OK;
+    case CURLE_OPERATION_TIMEDOUT:
+        return dsn::ERR_TIMEOUT;
+    default:
+        return dsn::ERR_CURL_FAILED;
+    }
+}
+
+} // anonymous namespace
+
+#define RETURN_IF_CURL_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);               
                    \
+        }                                                                      
                    \
+    } 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_GETINFO_NOT_OK(info, output)                                 
                    \
+    RETURN_IF_CURL_NOT_OK(curl_easy_getinfo(_curl, 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)
+
+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");
+        }
+    } else {
+        curl_easy_reset(_curl);
+    }
+
+    clear_header_fields();
+    free_header_list();
+
+    // Additional messages for errors are needed.
+    clear_error_buf();
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_ERRORBUFFER, _error_buf);
+
+    // Set with NOSIGNAL since we are multi-threaded.
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_NOSIGNAL, 1L);
+
+    // Redirects are supported.
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_FOLLOWLOCATION, 1L);
+
+    // Before 8.3.0, CURLOPT_MAXREDIRS was unlimited.
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_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));
+
+    // 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
+    // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
+    curl_write_callback callback = [](char *buffer, size_t size, size_t nmemb, 
void *param) {
+        http_client *client = reinterpret_cast<http_client *>(param);
+        return client->on_response_data(buffer, size * nmemb);
+    };
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_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 dsn::error_s::ok();
+}
+
+void http_client::clear_error_buf() { _error_buf[0] = 0; }
+
+bool http_client::is_error_buf_empty() const { return _error_buf[0] == 0; }
+
+std::string http_client::to_error_msg(CURLcode code) const
+{
+    std::string err_msg =
+        fmt::format("code={}, desc=\"{}\"", static_cast<int>(code), 
curl_easy_strerror(code));
+    if (is_error_buf_empty()) {
+        return err_msg;
+    }
+
+    err_msg += fmt::format(", msg=\"{}\"", _error_buf);
+    return err_msg;
+}
+
+// `data` passed to this function is NOT null-terminated.
+// `length` might be zero.
+size_t http_client::on_response_data(const void *data, size_t length)
+{
+    if (_recv_callback == nullptr) {
+        return length;
+    }
+
+    if (!(*_recv_callback)) {
+        // callback function is empty.
+        return length;
+    }
+
+    // According to libcurl, callback should return the number of bytes 
actually taken care of.
+    // If that amount differs from the amount passed to callback function, it 
would signals an
+    // error condition. This causes the transfer to get aborted and the 
libcurl function used
+    // returns CURLE_WRITE_ERROR. Therefore, here we just return the max limit 
of size_t for
+    // failure.
+    //
+    // See https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html for details.
+    return (*_recv_callback)(data, length) ? length : 
std::numeric_limits<size_t>::max();
+}
+
+dsn::error_s http_client::set_url(const std::string &url)
+{
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_URL, url.c_str());
+
+    _url = url;
+    return dsn::error_s::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.
+    // 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());
+    _method = http_method::POST;
+    return dsn::error_s::ok();
+}
+
+dsn::error_s http_client::with_get_method() { return 
set_method(http_method::GET); }
+
+dsn::error_s http_client::set_method(http_method method)
+{
+    // No need to process the case of http_method::POST, since it should be 
enabled by
+    // `with_post_method`.
+    switch (method) {
+    case http_method::GET:
+        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPGET, 1L);
+        break;
+    default:
+        LOG_FATAL("Unsupported http_method");
+    }
+
+    _method = method;
+    return dsn::error_s::ok();
+}
+
+dsn::error_s http_client::set_timeout(long timeout_ms)
+{
+    RETURN_IF_SETOPT_NOT_OK(CURLOPT_TIMEOUT_MS, timeout_ms);
+    return dsn::error_s::ok();
+}
+
+void http_client::clear_header_fields()
+{
+    _header_fields.clear();
+
+    _header_changed = true;
+}
+
+void http_client::free_header_list()
+{
+    if (_header_list == nullptr) {
+        return;
+    }
+
+    curl_slist_free_all(_header_list);
+    _header_list = nullptr;
+}
+
+void http_client::set_header_field(dsn::string_view key, dsn::string_view val)
+{
+    _header_fields[std::string(key)] = std::string(val);
+    _header_changed = true;
+}
+
+void http_client::set_accept(dsn::string_view val) { 
set_header_field("Accept", val); }
+
+void http_client::set_content_type(dsn::string_view val) { 
set_header_field("Content-Type", val); }
+
+dsn::error_s http_client::process_header()
+{
+    if (!_header_changed) {
+        return dsn::error_s::ok();
+    }
+
+    free_header_list();
+
+    for (const auto &field : _header_fields) {
+        auto str = fmt::format("{}: {}", field.first, field.second);
+
+        // A null pointer is returned if anything went wrong, otherwise the 
new list pointer is
+        // returned. To avoid overwriting an existing non-empty list on 
failure, the new list
+        // should be returned to a temporary variable which can be tested for 
NULL before updating
+        // the original list pointer. 
(https://curl.se/libcurl/c/curl_slist_append.html)
+        struct curl_slist *temp = curl_slist_append(_header_list, str.c_str());
+        if (temp == nullptr) {
+            free_header_list();
+            return dsn::error_s::make(dsn::ERR_CURL_FAILED, "curl_slist_append 
failed");
+        }
+        _header_list = temp;
+    }
+
+    // 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);
+
+    // New header has been built successfully, thus mark it unchanged.
+    _header_changed = false;
+
+    return dsn::error_s::ok();
+}
+
+dsn::error_s http_client::exec_method(const http_client::recv_callback 
&callback)
+{
+    // `curl_easy_perform` would run synchronously, thus it is safe to use the 
pointer to
+    // `callback`.
+    _recv_callback = &callback;
+
+    RETURN_NOT_OK(process_header());
+
+    RETURN_IF_EXEC_METHOD_NOT_OK();
+    return dsn::error_s::ok();
+}
+
+dsn::error_s http_client::exec_method(std::string *response)
+{
+    if (response == nullptr) {
+        return exec_method();
+    }
+
+    auto callback = [response](const void *data, size_t length) {
+        response->append(reinterpret_cast<const char *>(data), length);
+        return true;
+    };
+
+    return exec_method(callback);
+}
+
+dsn::error_s http_client::get_http_status(long &http_status) const
+{
+    RETURN_IF_GETINFO_NOT_OK(CURLINFO_RESPONSE_CODE, &http_status);
+    return dsn::error_s::ok();
+}
+
+#undef RETURN_IF_EXEC_METHOD_NOT_OK
+#undef RETURN_IF_SETOPT_NOT_OK
+#undef RETURN_IF_CURL_NOT_OK
+
+} // namespace dsn
diff --git a/src/http/http_client.h b/src/http/http_client.h
new file mode 100644
index 000000000..190af11f2
--- /dev/null
+++ b/src/http/http_client.h
@@ -0,0 +1,154 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <curl/curl.h>
+#include <stddef.h>
+#include <functional>
+#include <string>
+#include <unordered_map>
+
+#include "http/http_method.h"
+#include "utils/errors.h"
+#include "utils/ports.h"
+#include "utils/string_view.h"
+
+namespace dsn {
+
+// A library for http client that provides convenient APIs to access http 
services, implemented
+// based on libcurl (https://curl.se/libcurl/c/).
+//
+// This class is not thread-safe. Thus maintain one instance for each thread.
+//
+// Example of submitting GET request to remote http service
+// --------------------------------------------------------
+// Create an instance of http_client:
+// http_client client;
+//
+// It's necessary to initialize the new instance before coming into use:
+// auto err = client.init();
+//
+// Specify the target url that you would request for:
+// err = client.set_url(method);
+//
+// If you would use GET method, call `with_get_method`:
+// err = client.with_get_method();
+//
+// If you would use POST method, call `with_post_method` with post data:
+// err = client.with_post_method(post_data);
+//
+// Submit the request to remote http service:
+// err = client.exec_method();
+//
+// If response data should be processed, use callback function:
+// auto callback = [...](const void *data, size_t length) {
+//     ......
+//     return true;
+// };
+// err = client.exec_method(callback);
+//
+// Or just provide a string pointer:
+// std::string response;
+// err = client.exec_method(&response);
+//
+// Get the http status code after requesting:
+// long http_status;
+// err = client.get_http_status(http_status);
+class http_client
+{
+public:
+    using recv_callback = std::function<bool(const void *data, size_t length)>;
+
+    http_client();
+    ~http_client();
+
+    // Before coming into use, init() must be called to initialize http 
client. It could also be
+    // called to reset the http clients that have been initialized previously.
+    dsn::error_s init();
+
+    // Specify the target url that the request would be sent for.
+    dsn::error_s set_url(const std::string &url);
+
+    // Using post method, with `data` as the payload for post body.
+    dsn::error_s with_post_method(const std::string &data);
+
+    // Using get method.
+    dsn::error_s with_get_method();
+
+    // Specify the maximum time in milliseconds that a request is allowed to 
complete.
+    dsn::error_s set_timeout(long timeout_ms);
+
+    // Operations for the header fields.
+    void clear_header_fields();
+    void set_accept(dsn::string_view val);
+    void set_content_type(dsn::string_view val);
+
+    // Submit request to remote http service, with response processed by 
callback function.
+    //
+    // `callback` function gets called by libcurl as soon as there is data 
received that needs
+    // to be saved. For most transfers, this callback gets called many times 
and each invoke
+    // delivers another chunk of data.
+    //
+    // This function would run synchronously, which means it would wait until 
the response was
+    // returned and processed appropriately.
+    dsn::error_s exec_method(const recv_callback &callback = {});
+
+    // Submit request to remote http service, with response data returned in a 
string.
+    //
+    // This function would run synchronously, which means it would wait until 
the response was
+    // returned and processed appropriately.
+    dsn::error_s exec_method(std::string *response);
+
+    // Get the last http status code after requesting.
+    dsn::error_s get_http_status(long &http_status) const;
+
+private:
+    using header_field_map = std::unordered_map<std::string, std::string>;
+
+    void clear_error_buf();
+    bool is_error_buf_empty() const;
+    std::string to_error_msg(CURLcode code) const;
+
+    size_t on_response_data(const void *data, size_t length);
+
+    // Specify which http method would be used, such as GET. Enabling POST 
should not use this
+    // function (use `with_post_method` instead).
+    dsn::error_s set_method(http_method method);
+
+    void free_header_list();
+    void set_header_field(dsn::string_view key, dsn::string_view val);
+    dsn::error_s process_header();
+
+    // The size of a buffer that is used by libcurl to store human readable
+    // error messages on failures or problems.
+    static const constexpr size_t kErrorBufferBytes = CURL_ERROR_SIZE;
+
+    CURL *_curl;
+    http_method _method;
+    std::string _url;
+    const recv_callback *_recv_callback;
+    char _error_buf[kErrorBufferBytes];
+
+    bool _header_changed;
+    header_field_map _header_fields;
+    struct curl_slist *_header_list;
+
+    DISALLOW_COPY_AND_ASSIGN(http_client);
+};
+
+} // namespace dsn
diff --git a/src/http/http_message_parser.cpp b/src/http/http_message_parser.cpp
index ce2e0054c..df873eea2 100644
--- a/src/http/http_message_parser.cpp
+++ b/src/http/http_message_parser.cpp
@@ -26,12 +26,13 @@
 
 #include "http_message_parser.h"
 
+#include <stdint.h>
 // IWYU pragma: no_include <ext/alloc_traits.h>
 #include <string.h>
 #include <utility>
 #include <vector>
 
-#include "http_server.h"
+#include "http/http_method.h"
 #include "nodejs/http_parser.h"
 #include "runtime/rpc/rpc_message.h"
 #include "utils/blob.h"
@@ -132,10 +133,10 @@ http_message_parser::http_message_parser()
 
         message_header *header = msg->header;
         if (parser->type == HTTP_REQUEST && parser->method == HTTP_GET) {
-            header->hdr_type = http_method::HTTP_METHOD_GET;
+            header->hdr_type = static_cast<uint32_t>(http_method::GET);
             header->context.u.is_request = 1;
         } else if (parser->type == HTTP_REQUEST && parser->method == 
HTTP_POST) {
-            header->hdr_type = http_method::HTTP_METHOD_POST;
+            header->hdr_type = static_cast<uint32_t>(http_method::POST);
             header->context.u.is_request = 1;
         } else {
             // Bit fields don't work with "perfect" forwarding, see
diff --git a/src/http/http_method.h b/src/http/http_method.h
new file mode 100644
index 000000000..f337d24e7
--- /dev/null
+++ b/src/http/http_method.h
@@ -0,0 +1,36 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include "utils/enum_helper.h"
+
+namespace dsn {
+
+enum class http_method
+{
+    GET = 1,
+    POST = 2,
+    INVALID = 100,
+};
+
+ENUM_BEGIN(http_method, http_method::INVALID)
+ENUM_REG2(http_method, GET)
+ENUM_REG2(http_method, POST)
+ENUM_END(http_method)
+
+} // namespace dsn
diff --git a/src/http/http_server.cpp b/src/http/http_server.cpp
index cbc179c81..4c2f70401 100644
--- a/src/http/http_server.cpp
+++ b/src/http/http_server.cpp
@@ -25,6 +25,7 @@
 
 #include "builtin_http_calls.h"
 #include "fmt/core.h"
+#include "http/http_method.h"
 #include "http_call_registry.h"
 #include "http_message_parser.h"
 #include "http_server_impl.h"
@@ -146,8 +147,8 @@ void http_server::serve(message_ex *msg)
         resp.body = fmt::format("failed to parse request: {}", 
res.get_error());
     } else {
         const http_request &req = res.get_value();
-        std::shared_ptr<http_call> call = 
http_call_registry::instance().find(req.path);
-        if (call != nullptr) {
+        auto call = http_call_registry::instance().find(req.path);
+        if (call) {
             call->callback(req, resp);
         } else {
             resp.status_code = http_status_code::not_found;
diff --git a/src/http/http_server.h b/src/http/http_server.h
index c84055261..5ae780b98 100644
--- a/src/http/http_server.h
+++ b/src/http/http_server.h
@@ -25,6 +25,7 @@
 #include <unordered_map>
 #include <utility>
 
+#include "http_method.h"
 #include "runtime/task/task_code.h"
 #include "utils/blob.h"
 #include "utils/errors.h"
@@ -38,12 +39,6 @@ DSN_DECLARE_bool(enable_http_server);
 /// The rpc code for all the HTTP RPCs.
 DEFINE_TASK_CODE_RPC(RPC_HTTP_SERVICE, TASK_PRIORITY_COMMON, 
THREAD_POOL_DEFAULT);
 
-enum http_method
-{
-    HTTP_METHOD_GET = 1,
-    HTTP_METHOD_POST = 2,
-};
-
 class message_ex;
 
 struct http_request
diff --git a/src/http/pprof_http_service.cpp b/src/http/pprof_http_service.cpp
index f57c57b19..f5aeb8b50 100644
--- a/src/http/pprof_http_service.cpp
+++ b/src/http/pprof_http_service.cpp
@@ -38,6 +38,7 @@
 #include <utility>
 #include <vector>
 
+#include "http/http_method.h"
 #include "http/http_server.h"
 #include "runtime/api_layer1.h"
 #include "utils/blob.h"
@@ -308,7 +309,7 @@ void pprof_http_service::symbol_handler(const http_request 
&req, http_response &
     // Load /proc/self/maps
     pthread_once(&s_load_symbolmap_once, load_symbols);
 
-    if (req.method != http_method::HTTP_METHOD_POST) {
+    if (req.method != http_method::POST) {
         char buf[64];
         snprintf(buf, sizeof(buf), "num_symbols: %lu\n", symbol_map.size());
         resp.body = buf;
diff --git a/src/http/test/CMakeLists.txt b/src/http/test/CMakeLists.txt
index d4da7e5b3..85f2c3798 100644
--- a/src/http/test/CMakeLists.txt
+++ b/src/http/test/CMakeLists.txt
@@ -24,14 +24,16 @@ set(MY_SRC_SEARCH_MODE "GLOB")
 set(MY_PROJ_LIBS
     dsn_http
     dsn_runtime
+    curl
     gtest
-    gtest_main
-    rocksdb)
+    rocksdb
+    )
 
 set(MY_BOOST_LIBS Boost::system Boost::filesystem Boost::regex)
 
 set(MY_BINPLACES
     "${CMAKE_CURRENT_SOURCE_DIR}/run.sh"
+    "${CMAKE_CURRENT_SOURCE_DIR}/config-test.ini"
 )
 
 dsn_add_test()
diff --git a/src/http/test/config-test.ini b/src/http/test/config-test.ini
new file mode 100644
index 000000000..744d8d412
--- /dev/null
+++ b/src/http/test/config-test.ini
@@ -0,0 +1,75 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements.  See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership.  The ASF licenses this file
+; to you under the Apache License, Version 2.0 (the
+; "License"); you may not use this file except in compliance
+; with the License.  You may obtain a copy of the License at
+;
+;   http://www.apache.org/licenses/LICENSE-2.0
+;
+; Unless required by applicable law or agreed to in writing,
+; software distributed under the License is distributed on an
+; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+; KIND, either express or implied.  See the License for the
+; specific language governing permissions and limitations
+; under the License.
+
+[apps..default]
+run = true
+count = 1
+network.client.RPC_CHANNEL_TCP = dsn::tools::asio_network_provider, 65536
+network.client.RPC_CHANNEL_UDP = dsn::tools::asio_udp_provider, 65536
+network.server.0.RPC_CHANNEL_TCP = dsn::tools::asio_network_provider, 65536
+network.server.0.RPC_CHANNEL_UDP = dsn::tools::asio_udp_provider, 65536
+
+[apps.test]
+type = test
+arguments =
+run = true
+ports = 20001
+count = 1
+pools = THREAD_POOL_DEFAULT
+
+[core]
+tool = nativerun
+
+toollets = tracer, profiler
+pause_on_start = false
+
+logging_start_level = LOG_LEVEL_DEBUG
+logging_factory_name = dsn::tools::simple_logger
+
+[tools.simple_logger]
+fast_flush = true
+short_header = false
+stderr_start_level = LOG_LEVEL_DEBUG
+
+[network]
+; how many network threads for network library (used by asio)
+io_service_worker_count = 2
+
+[task..default]
+is_trace = true
+is_profile = true
+allow_inline = false
+rpc_call_channel = RPC_CHANNEL_TCP
+rpc_message_header_format = dsn
+rpc_timeout_milliseconds = 1000
+
+[task.LPC_RPC_TIMEOUT]
+is_trace = false
+is_profile = false
+
+[task.RPC_TEST_UDP]
+rpc_call_channel = RPC_CHANNEL_UDP
+rpc_message_crc_required = true
+
+; specification for each thread pool
+[threadpool..default]
+worker_count = 2
+
+[threadpool.THREAD_POOL_DEFAULT]
+partitioned = false
+worker_priority = THREAD_xPRIORITY_NORMAL
+
diff --git a/src/http/test/http_client_test.cpp 
b/src/http/test/http_client_test.cpp
new file mode 100644
index 000000000..d15cb7bdd
--- /dev/null
+++ b/src/http/test/http_client_test.cpp
@@ -0,0 +1,213 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <fmt/core.h>
+#include <gtest/gtest.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 <vector>
+
+#include "http/http_client.h"
+#include "http/http_method.h"
+#include "utils/error_code.h"
+#include "utils/errors.h"
+#include "utils/fmt_logging.h"
+#include "utils/test_macros.h"
+
+namespace dsn {
+
+void check_expected_description_prefix(const std::string 
&expected_description_prefix,
+                                       const dsn::error_s &err)
+{
+    const std::string actual_description(err.description());
+    std::cout << actual_description << std::endl;
+
+    ASSERT_LT(expected_description_prefix.size(), actual_description.size());
+    EXPECT_EQ(expected_description_prefix,
+              actual_description.substr(0, 
expected_description_prefix.size()));
+}
+
+TEST(HttpClientTest, Connect)
+{
+    http_client client;
+    ASSERT_TRUE(client.init());
+
+    // No one has listened on port 20000, thus this would lead to "Connection 
refused".
+    ASSERT_TRUE(client.set_url("http://127.0.0.1:20000/test/get";));
+
+    const auto &err = client.exec_method();
+    ASSERT_EQ(dsn::ERR_CURL_FAILED, err.code());
+
+    std::cout << "failed to connect: ";
+
+    // "code=7" means CURLE_COULDNT_CONNECT, see 
https://curl.se/libcurl/c/libcurl-errors.html
+    // for details.
+    //
+    // We just check the prefix of description, including `method`, `url`, 
`code` and `desc`.
+    // The `msg` differ in various systems, such as:
+    // * msg="Failed to connect to 127.0.0.1 port 20000: Connection refused"
+    // * msg="Failed to connect to 127.0.0.1 port 20000 after 0 ms: Connection 
refused"
+    // Thus we don't check if `msg` fields are consistent.
+    const std::string expected_description_prefix(
+        "ERR_CURL_FAILED: failed to perform http request("
+        "method=GET, url=http://127.0.0.1:20000/test/get): code=7, "
+        "desc=\"Couldn't connect to server\"");
+    NO_FATALS(check_expected_description_prefix(expected_description_prefix, 
err));
+}
+
+TEST(HttpClientTest, Callback)
+{
+    http_client client;
+    ASSERT_TRUE(client.init());
+
+    ASSERT_TRUE(client.set_url("http://127.0.0.1:20001/test/get";));
+    ASSERT_TRUE(client.with_get_method());
+
+    auto callback = [](const void *, size_t) { return false; };
+
+    const auto &err = client.exec_method(callback);
+    ASSERT_EQ(dsn::ERR_CURL_FAILED, err.code());
+
+    long actual_http_status;
+    ASSERT_TRUE(client.get_http_status(actual_http_status));
+    EXPECT_EQ(200, actual_http_status);
+
+    std::cout << "failed for callback: ";
+
+    // "code=23" means CURLE_WRITE_ERROR, see 
https://curl.se/libcurl/c/libcurl-errors.html
+    // for details.
+    //
+    // We just check the prefix of description, including `method`, `url`, 
`code` and `desc`.
+    // The `msg` differ in various systems, such as:
+    // * msg="Failed writing body (18446744073709551615 != 24)"
+    // * msg="Failure writing output to destination"
+    // Thus we don't check if `msg` fields are consistent.
+    const auto expected_description_prefix =
+        fmt::format("ERR_CURL_FAILED: failed to perform http request("
+                    "method=GET, url=http://127.0.0.1:20001/test/get): 
code=23, "
+                    "desc=\"Failed writing received data to 
disk/application\"");
+    NO_FATALS(check_expected_description_prefix(expected_description_prefix, 
err));
+}
+
+using http_client_method_case =
+    std::tuple<const char *, http_method, const char *, long, const char *>;
+
+class HttpClientMethodTest : public 
testing::TestWithParam<http_client_method_case>
+{
+public:
+    void SetUp() override { ASSERT_TRUE(_client.init()); }
+
+    void test_method_with_response_string(const long expected_http_status,
+                                          const std::string &expected_response)
+    {
+        std::string actual_response;
+        ASSERT_TRUE(_client.exec_method(&actual_response));
+
+        long actual_http_status;
+        ASSERT_TRUE(_client.get_http_status(actual_http_status));
+
+        EXPECT_EQ(expected_http_status, actual_http_status);
+        EXPECT_EQ(expected_response, actual_response);
+    }
+
+    void test_method_with_response_callback(const long expected_http_status,
+                                            const std::string 
&expected_response)
+    {
+        auto callback = [&expected_response](const void *data, size_t length) {
+            auto compare = [](const char *expected_data,
+                              size_t expected_length,
+                              const void *actual_data,
+                              size_t actual_length) {
+                if (expected_length != actual_length) {
+                    return false;
+                }
+                return std::memcmp(expected_data, actual_data, actual_length) 
== 0;
+            };
+            EXPECT_PRED4(compare, expected_response.data(), 
expected_response.size(), data, length);
+            return true;
+        };
+        ASSERT_TRUE(_client.exec_method(callback));
+
+        long actual_http_status;
+        ASSERT_TRUE(_client.get_http_status(actual_http_status));
+        EXPECT_EQ(expected_http_status, actual_http_status);
+    }
+
+    void test_mothod(const std::string &url,
+                     const http_method method,
+                     const std::string &post_data,
+                     const long expected_http_status,
+                     const std::string &expected_response)
+    {
+        _client.set_url(url);
+
+        switch (method) {
+        case http_method::GET:
+            ASSERT_TRUE(_client.with_get_method());
+            break;
+        case http_method::POST:
+            ASSERT_TRUE(_client.with_post_method(post_data));
+            break;
+        default:
+            LOG_FATAL("Unsupported http_method");
+        }
+
+        test_method_with_response_string(expected_http_status, 
expected_response);
+        test_method_with_response_callback(expected_http_status, 
expected_response);
+    }
+
+private:
+    http_client _client;
+};
+
+TEST_P(HttpClientMethodTest, ExecMethod)
+{
+    const char *url;
+    http_method method;
+    const char *post_data;
+    long expected_http_status;
+    const char *expected_response;
+    std::tie(url, method, post_data, expected_http_status, expected_response) 
= GetParam();
+
+    http_client _client;
+    test_mothod(url, method, post_data, expected_http_status, 
expected_response);
+}
+
+const std::vector<http_client_method_case> http_client_method_tests = {
+    {"http://127.0.0.1:20001/test/get";,
+     http_method::POST,
+     "with POST DATA",
+     400,
+     "please use GET method"},
+    {"http://127.0.0.1:20001/test/get";, http_method::GET, "", 200, "you are 
using GET method"},
+    {"http://127.0.0.1:20001/test/post";,
+     http_method::POST,
+     "with POST DATA",
+     200,
+     "you are using POST method with POST DATA"},
+    {"http://127.0.0.1:20001/test/post";, http_method::GET, "", 400, "please 
use POST method"},
+};
+
+INSTANTIATE_TEST_CASE_P(HttpClientTest,
+                        HttpClientMethodTest,
+                        testing::ValuesIn(http_client_method_tests));
+
+} // namespace dsn
diff --git a/src/http/test/http_server_test.cpp 
b/src/http/test/http_server_test.cpp
index 9aa9f17c3..0c6b9ec6d 100644
--- a/src/http/test/http_server_test.cpp
+++ b/src/http/test/http_server_test.cpp
@@ -18,6 +18,7 @@
 // IWYU pragma: no_include <gtest/gtest-message.h>
 // IWYU pragma: no_include <gtest/gtest-test-part.h>
 #include <gtest/gtest.h>
+#include <stdint.h>
 #include <string.h>
 #include <algorithm>
 #include <memory>
@@ -29,6 +30,7 @@
 #include "http/builtin_http_calls.h"
 #include "http/http_call_registry.h"
 #include "http/http_message_parser.h"
+#include "http/http_method.h"
 #include "http/http_server.h"
 #include "runtime/rpc/message_parser.h"
 #include "runtime/rpc/rpc_message.h"
@@ -87,7 +89,12 @@ TEST(bultin_http_calls_test, meta_query)
 
 TEST(bultin_http_calls_test, get_help)
 {
+    // Used to save current http calls as backup.
+    std::vector<std::shared_ptr<http_call>> backup_calls;
+
+    // Remove all http calls.
     for (const auto &call : http_call_registry::instance().list_all_calls()) {
+        backup_calls.push_back(call);
         http_call_registry::instance().remove(call->path);
     }
 
@@ -111,9 +118,15 @@ TEST(bultin_http_calls_test, get_help)
     get_help_handler(req, resp);
     ASSERT_EQ(resp.body, 
"{\"/\":\"ip:port/\",\"/recentStartTime\":\"ip:port/recentStartTime\"}\n");
 
+    // Remove all http calls, especially `recentStartTime`.
     for (const auto &call : http_call_registry::instance().list_all_calls()) {
         http_call_registry::instance().remove(call->path);
     }
+
+    // Recover http calls from backup.
+    for (const auto &call : backup_calls) {
+        http_call_registry::instance().add(call);
+    }
 }
 
 class http_message_parser_test : public testing::Test
@@ -174,7 +187,7 @@ public:
             message_ptr msg = parser.get_message_on_receive(&reader, 
read_next);
             ASSERT_NE(msg, nullptr);
             ASSERT_EQ(msg->hdr_format, NET_HDR_HTTP);
-            ASSERT_EQ(msg->header->hdr_type, http_method::HTTP_METHOD_GET);
+            ASSERT_EQ(msg->header->hdr_type, 
static_cast<uint32_t>(http_method::GET));
             ASSERT_EQ(msg->header->context.u.is_request, 1);
             ASSERT_EQ(msg->buffers.size(), HTTP_MSG_BUFFERS_NUM);
             ASSERT_EQ(msg->buffers[2].size(), 1); // url
@@ -215,7 +228,7 @@ TEST_F(http_message_parser_test, parse_request)
     ASSERT_NE(msg, nullptr);
 
     ASSERT_EQ(msg->hdr_format, NET_HDR_HTTP);
-    ASSERT_EQ(msg->header->hdr_type, http_method::HTTP_METHOD_POST);
+    ASSERT_EQ(msg->header->hdr_type, static_cast<uint32_t>(http_method::POST));
     ASSERT_EQ(msg->header->context.u.is_request, 1);
     ASSERT_EQ(msg->buffers.size(), HTTP_MSG_BUFFERS_NUM);
     ASSERT_EQ(msg->buffers[1].to_string(), "Message Body sdfsdf"); // body
@@ -266,7 +279,7 @@ TEST_F(http_message_parser_test, eof)
     ASSERT_NE(msg, nullptr);
 
     ASSERT_EQ(msg->hdr_format, NET_HDR_HTTP);
-    ASSERT_EQ(msg->header->hdr_type, http_method::HTTP_METHOD_GET);
+    ASSERT_EQ(msg->header->hdr_type, static_cast<uint32_t>(http_method::GET));
     ASSERT_EQ(msg->header->context.u.is_request, 1);
     ASSERT_EQ(msg->buffers.size(), HTTP_MSG_BUFFERS_NUM);
     ASSERT_EQ(msg->buffers[1].to_string(), ""); // body
@@ -297,7 +310,7 @@ TEST_F(http_message_parser_test, parse_long_url)
     message_ptr msg = parser.get_message_on_receive(&reader, read_next);
     ASSERT_NE(msg, nullptr);
     ASSERT_EQ(msg->hdr_format, NET_HDR_HTTP);
-    ASSERT_EQ(msg->header->hdr_type, http_method::HTTP_METHOD_GET);
+    ASSERT_EQ(msg->header->hdr_type, static_cast<uint32_t>(http_method::GET));
     ASSERT_EQ(msg->header->context.u.is_request, 1);
     ASSERT_EQ(msg->buffers.size(), HTTP_MSG_BUFFERS_NUM);
     ASSERT_EQ(msg->buffers[2].size(), 4097); // url
diff --git a/src/http/test/main.cpp b/src/http/test/main.cpp
new file mode 100644
index 000000000..1eeb3d04e
--- /dev/null
+++ b/src/http/test/main.cpp
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <fmt/core.h>
+#include <gtest/gtest.h>
+#include <chrono>
+#include <functional>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "http/http_method.h"
+#include "http/http_server.h"
+#include "runtime/app_model.h"
+#include "runtime/service_app.h"
+#include "utils/blob.h"
+#include "utils/error_code.h"
+#include "utils/ports.h"
+
+int gtest_flags = 0;
+int gtest_ret = 0;
+
+class test_http_service : public dsn::http_server_base
+{
+public:
+    test_http_service()
+    {
+        register_handler("get",
+                         std::bind(&test_http_service::method_handler,
+                                   this,
+                                   dsn::http_method::GET,
+                                   std::placeholders::_1,
+                                   std::placeholders::_2),
+                         "ip:port/test/get");
+        register_handler("post",
+                         std::bind(&test_http_service::method_handler,
+                                   this,
+                                   dsn::http_method::POST,
+                                   std::placeholders::_1,
+                                   std::placeholders::_2),
+                         "ip:port/test/post");
+    }
+
+    ~test_http_service() = default;
+
+    std::string path() const override { return "test"; }
+
+private:
+    void method_handler(dsn::http_method target_method,
+                        const dsn::http_request &req,
+                        dsn::http_response &resp)
+    {
+        if (req.method != target_method) {
+            resp.body = fmt::format("please use {} method", 
enum_to_string(target_method));
+            resp.status_code = dsn::http_status_code::bad_request;
+            return;
+        }
+
+        std::string postfix;
+        if (target_method == dsn::http_method::POST) {
+            postfix = " ";
+            postfix += req.body.to_string();
+        }
+
+        resp.body =
+            fmt::format("you are using {} method{}", 
enum_to_string(target_method), postfix);
+        resp.status_code = dsn::http_status_code::ok;
+    }
+
+    DISALLOW_COPY_AND_ASSIGN(test_http_service);
+};
+
+class test_service_app : public dsn::service_app
+{
+public:
+    test_service_app(const dsn::service_app_info *info) : 
dsn::service_app(info)
+    {
+        dsn::register_http_service(new test_http_service);
+        dsn::start_http_server();
+    }
+
+    dsn::error_code start(const std::vector<std::string> &args) override
+    {
+        gtest_ret = RUN_ALL_TESTS();
+        gtest_flags = 1;
+        return dsn::ERR_OK;
+    }
+};
+
+GTEST_API_ int main(int argc, char **argv)
+{
+    testing::InitGoogleTest(&argc, argv);
+
+    // Register test service.
+    dsn::service_app::register_factory<test_service_app>("test");
+
+    dsn_run_config("config-test.ini", false);
+    while (gtest_flags == 0) {
+        std::this_thread::sleep_for(std::chrono::seconds(1));
+    }
+
+#ifndef ENABLE_GCOV
+    dsn_exit(gtest_ret);
+#endif
+    return gtest_ret;
+}
diff --git a/src/utils/error_code.h b/src/utils/error_code.h
index 45a6e793c..04df97947 100644
--- a/src/utils/error_code.h
+++ b/src/utils/error_code.h
@@ -180,6 +180,8 @@ DEFINE_ERR_CODE(ERR_RANGER_POLICIES_NO_NEED_UPDATE)
 DEFINE_ERR_CODE(ERR_RDB_CORRUPTION)
 
 DEFINE_ERR_CODE(ERR_DISK_IO_ERROR)
+
+DEFINE_ERR_CODE(ERR_CURL_FAILED)
 } // namespace dsn
 
 USER_DEFINED_STRUCTURE_FORMATTER(::dsn::error_code);
diff --git a/src/utils/errors.h b/src/utils/errors.h
index 9cbf70e09..ce36befca 100644
--- a/src/utils/errors.h
+++ b/src/utils/errors.h
@@ -97,6 +97,8 @@ public:
         return true;
     }
 
+    explicit operator bool() const noexcept { return is_ok(); }
+
     std::string description() const
     {
         if (!_info) {
@@ -227,7 +229,7 @@ USER_DEFINED_STRUCTURE_FORMATTER(::dsn::error_s);
 #define RETURN_NOT_OK(s)                                                       
                    \
     do {                                                                       
                    \
         const ::dsn::error_s &_s = (s);                                        
                    \
-        if (dsn_unlikely(!_s.is_ok())) {                                       
                    \
+        if (dsn_unlikely(!_s)) {                                               
                    \
             return _s;                                                         
                    \
         }                                                                      
                    \
     } while (false);
diff --git a/src/utils/metrics.cpp b/src/utils/metrics.cpp
index 40caace7a..b5800d3cd 100644
--- a/src/utils/metrics.cpp
+++ b/src/utils/metrics.cpp
@@ -23,6 +23,7 @@
 #include <fmt/core.h>
 #include <new>
 
+#include "http/http_method.h"
 #include "runtime/api_layer1.h"
 #include "utils/flags.h"
 #include "utils/rand.h"
@@ -275,7 +276,7 @@ const dsn::metric_filters::metric_fields_type 
kBriefMetricFields = get_brief_met
 
 void metrics_http_service::get_metrics_handler(const http_request &req, 
http_response &resp)
 {
-    if (req.method != http_method::HTTP_METHOD_GET) {
+    if (req.method != http_method::GET) {
         resp.body = encode_error_as_json("please use 'GET' method while 
querying for metrics");
         resp.status_code = http_status_code::bad_request;
         return;
diff --git a/src/utils/test/metrics_test.cpp b/src/utils/test/metrics_test.cpp
index b2118f03e..8d65c65da 100644
--- a/src/utils/test/metrics_test.cpp
+++ b/src/utils/test/metrics_test.cpp
@@ -2513,7 +2513,7 @@ void test_http_get_metrics(const std::string 
&request_string,
     ASSERT_TRUE(req_res.is_ok());
 
     const auto &req = req_res.get_value();
-    std::cout << "method: " << req.method << std::endl;
+    std::cout << "method: " << enum_to_string(req.method) << std::endl;
 
     http_response resp;
     test_get_metrics_handler(req, resp);
diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt
index 4558aa94f..f6fd11d4b 100644
--- a/thirdparty/CMakeLists.txt
+++ b/thirdparty/CMakeLists.txt
@@ -276,11 +276,7 @@ ExternalProject_Add(civetweb
 ExternalProject_Get_property(civetweb SOURCE_DIR)
 set(civetweb_SRC ${SOURCE_DIR})
 
-ExternalProject_Add(curl
-        URL ${OSS_URL_PREFIX}/curl-7.47.0.tar.gz
-        http://curl.haxx.se/download/curl-7.47.0.tar.gz
-        URL_MD5 5109d1232d208dfd712c0272b8360393
-        CONFIGURE_COMMAND ./configure --prefix=${TP_OUTPUT}
+set(CURL_OPTIONS
         --disable-dict
         --disable-file
         --disable-ftp
@@ -301,6 +297,19 @@ ExternalProject_Add(curl
         --without-libssh2
         --without-ssl
         --without-libidn
+        )
+if (APPLE)
+    set(CURL_OPTIONS
+           ${CURL_OPTIONS}
+           --without-nghttp2
+           )
+endif ()
+ExternalProject_Add(curl
+        URL ${OSS_URL_PREFIX}/curl-7.47.0.tar.gz
+        http://curl.haxx.se/download/curl-7.47.0.tar.gz
+        URL_MD5 5109d1232d208dfd712c0272b8360393
+        CONFIGURE_COMMAND ./configure --prefix=${TP_OUTPUT}
+        ${CURL_OPTIONS}
         BUILD_IN_SOURCE 1
         )
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to