[util] add error buffer for EasyCurl wrapper

Enabled CURLOPT_ERRORBUFFER option for the EasyCurl utility wrapper.
This is to help collecting additional information on the error occurred,
if any.

Change-Id: I6e4a8a8a42671f141842ff6051aa18820a8f7f5a
Reviewed-on: http://gerrit.cloudera.org:8080/15101
Tested-by: Kudu Jenkins
Reviewed-by: Adar Dembo <adar@cloudera.com>
Reviewed-by: Volodymyr Verovkin <verjovkin@cloudera.com>
diff --git a/src/kudu/util/curl_util.cc b/src/kudu/util/curl_util.cc
index aeb61dd..d75cc54 100644
--- a/src/kudu/util/curl_util.cc
+++ b/src/kudu/util/curl_util.cc
@@ -17,8 +17,8 @@
 
 #include "kudu/util/curl_util.h"
 
-#include <cstddef>
 #include <cstdint>
+#include <cstring>
 #include <mutex>
 #include <ostream>
 #include <string>
@@ -34,19 +34,26 @@
 
 using std::string;
 using std::vector;
+using strings::Substitute;
 
 namespace kudu {
 
 namespace {
 
-inline Status TranslateError(CURLcode code) {
+inline Status TranslateError(CURLcode code, const char* errbuf) {
   if (code == CURLE_OK) {
     return Status::OK();
   }
-  if (code == CURLE_OPERATION_TIMEDOUT) {
-    return Status::TimedOut("curl timeout", curl_easy_strerror(code));
+
+  string err_msg = curl_easy_strerror(code);
+  if (strlen(errbuf) != 0) {
+    err_msg += Substitute(": $0", errbuf);
   }
-  return Status::NetworkError("curl error", curl_easy_strerror(code));
+
+  if (code == CURLE_OPERATION_TIMEDOUT) {
+    return Status::TimedOut("curl timeout", err_msg);
+  }
+  return Status::NetworkError("curl error", err_msg);
 }
 
 extern "C" {
@@ -60,6 +67,10 @@
 
 } // anonymous namespace
 
+// This is an internal EasyCurl's utility macro.
+#define CURL_RETURN_NOT_OK(expr) \
+  RETURN_NOT_OK(TranslateError((expr), errbuf_))
+
 EasyCurl::EasyCurl() {
   // Use our own SSL initialization, and disable curl's.
   // Both of these calls are idempotent.
@@ -73,6 +84,12 @@
   });
   curl_ = curl_easy_init();
   CHECK(curl_) << "Could not init curl";
+
+  // Set the error buffer to enhance error messages with more details, when
+  // available.
+  static_assert(kErrBufSize >= CURL_ERROR_SIZE, "kErrBufSize is too small");
+  const auto code = curl_easy_setopt(curl_, CURLOPT_ERRORBUFFER, errbuf_);
+  CHECK_EQ(CURLE_OK, code);
 }
 
 EasyCurl::~EasyCurl() {
@@ -96,25 +113,24 @@
                            const vector<string>& headers) {
   CHECK_NOTNULL(dst)->clear();
 
+  // Mark the error buffer as cleared.
+  errbuf_[0] = 0;
+
   if (!verify_peer_) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(
-        curl_, CURLOPT_SSL_VERIFYHOST, 0)));
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(
-        curl_, CURLOPT_SSL_VERIFYPEER, 0)));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0));
   }
 
   if (use_spnego_) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(
-        curl_, CURLOPT_HTTPAUTH, CURLAUTH_NEGOTIATE)));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(
+        curl_, CURLOPT_HTTPAUTH, CURLAUTH_NEGOTIATE));
     // It's necessary to pass an empty user/password to trigger the authentication
     // code paths in curl, even though SPNEGO doesn't use them.
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(
-        curl_, CURLOPT_USERPWD, ":")));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_USERPWD, ":"));
   }
 
   if (verbose_) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(
-        curl_, CURLOPT_VERBOSE, 1)));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_VERBOSE, 1));
   }
 
   // Add headers if specified.
@@ -126,18 +142,17 @@
   for (const auto& header : headers) {
     curl_headers = CHECK_NOTNULL(curl_slist_append(curl_headers, header.c_str()));
   }
-  RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curl_headers)));
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curl_headers));
 
-  RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_URL, url.c_str())));
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()));
   if (return_headers_) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_HEADER, 1)));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_HEADER, 1));
   }
-  RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback)));
-  RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_WRITEDATA,
-                                                static_cast<void *>(dst))));
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback));
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_WRITEDATA, static_cast<void *>(dst)));
   if (post_data) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_POSTFIELDS,
-                                                  post_data->c_str())));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(
+        curl_, CURLOPT_POSTFIELDS, post_data->c_str()));
   }
 
   // Done after CURLOPT_POSTFIELDS in case that resets the method (the docs[1]
@@ -145,24 +160,24 @@
   //
   // 1. https://curl.haxx.se/libcurl/c/CURLOPT_POSTFIELDS.html
   if (!custom_method_.empty()) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST,
-                                                  custom_method_.c_str())));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(
+        curl_, CURLOPT_CUSTOMREQUEST, custom_method_.c_str()));
   }
 
-  RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_HTTPAUTH, CURLAUTH_ANY)));
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_HTTPAUTH, CURLAUTH_ANY));
   if (timeout_.Initialized()) {
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1)));
-    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS,
-        timeout_.ToMilliseconds())));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(
+        curl_, CURLOPT_TIMEOUT_MS, timeout_.ToMilliseconds()));
   }
-  RETURN_NOT_OK(TranslateError(curl_easy_perform(curl_)));
+  CURL_RETURN_NOT_OK(curl_easy_perform(curl_));
   long val; // NOLINT(*) curl wants a long
-  RETURN_NOT_OK(TranslateError(curl_easy_getinfo(curl_, CURLINFO_NUM_CONNECTS, &val)));
-  num_connects_ = val;
+  CURL_RETURN_NOT_OK(curl_easy_getinfo(curl_, CURLINFO_NUM_CONNECTS, &val));
+  num_connects_ = static_cast<int>(val);
 
-  RETURN_NOT_OK(TranslateError(curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &val)));
+  CURL_RETURN_NOT_OK(curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &val));
   if (val != 200) {
-    return Status::RemoteError(strings::Substitute("HTTP $0", val));
+    return Status::RemoteError(Substitute("HTTP $0", val));
   }
   return Status::OK();
 }
diff --git a/src/kudu/util/curl_util.h b/src/kudu/util/curl_util.h
index 2289e84..4ed07a0 100644
--- a/src/kudu/util/curl_util.h
+++ b/src/kudu/util/curl_util.h
@@ -14,9 +14,10 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-#ifndef KUDU_UTIL_CURL_UTIL_H
-#define KUDU_UTIL_CURL_UTIL_H
 
+#pragma once
+
+#include <cstddef>
 #include <string>
 #include <utility>
 #include <vector>
@@ -89,12 +90,15 @@
   }
 
  private:
+  static const constexpr size_t kErrBufSize = 256;
+
   // Do a request. If 'post_data' is non-NULL, does a POST.
   // Otherwise, does a GET.
   Status DoRequest(const std::string& url,
                    const std::string* post_data,
                    faststring* dst,
                    const std::vector<std::string>& headers = {});
+
   CURL* curl_;
 
   std::string custom_method_;
@@ -113,9 +117,9 @@
 
   int num_connects_ = 0;
 
+  char errbuf_[kErrBufSize];
+
   DISALLOW_COPY_AND_ASSIGN(EasyCurl);
 };
 
 } // namespace kudu
-
-#endif /* KUDU_UTIL_CURL_UTIL_H */