diff Core/HttpClient.cpp @ 3393:2cd0369a156f

support of chunked answers in HttpClient and in SDK
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 06 Jun 2019 16:12:55 +0200
parents ad434967a68c
children 6add197274b1
line wrap: on
line diff
--- a/Core/HttpClient.cpp	Thu Jun 06 10:35:16 2019 +0200
+++ b/Core/HttpClient.cpp	Thu Jun 06 16:12:55 2019 +0200
@@ -201,11 +201,11 @@
   };
 
 
-  class HttpClient::CurlRequestChunkedBody : public boost::noncopyable
+  class HttpClient::CurlRequestBody : public boost::noncopyable
   {
   private:
-    HttpClient::IRequestChunkedBody*  body_;
-    std::string                       buffer_;
+    HttpClient::IRequestBody*  body_;
+    std::string                buffer_;
 
     size_t CallbackInternal(char* curlBuffer,
                             size_t curlBufferSize)
@@ -243,12 +243,12 @@
     }
     
   public:
-    CurlRequestChunkedBody() :
+    CurlRequestBody() :
       body_(NULL)
     {
     }
 
-    void SetBody(HttpClient::IRequestChunkedBody& body)
+    void SetBody(HttpClient::IRequestBody& body)
     {
       body_ = &body;
       buffer_.clear();
@@ -265,22 +265,79 @@
       return body_ != NULL;
     }
 
-    static size_t Callback(char *buffer,
-                           size_t size,
-                           size_t nitems,
-                           void *userdata)
+    static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata)
     {
       try
       {
-        HttpClient::CurlRequestChunkedBody* body = reinterpret_cast<HttpClient::CurlRequestChunkedBody*>(userdata);
+        assert(userdata != NULL);
+        return reinterpret_cast<HttpClient::CurlRequestBody*>(userdata)->
+          CallbackInternal(buffer, size * nitems);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
 
-        if (body == NULL)
+  class HttpClient::CurlAnswer : public boost::noncopyable
+  {
+  private:
+    HttpClient::IAnswer&  answer_;
+    bool                  headersLowerCase_;
+
+  public:
+    CurlAnswer(HttpClient::IAnswer& answer,
+               bool headersLowerCase) :
+      answer_(answer),
+      headersLowerCase_(headersLowerCase)
+    {
+    }
+
+    static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
+
+        size_t length = size * nmemb;
+        if (length == 0)
         {
-          throw OrthancException(ErrorCode_NullPointer);
+          return 0;
         }
         else
         {
-          return body->CallbackInternal(buffer, size * nitems);
+          std::string s(reinterpret_cast<const char*>(buffer), length);
+          std::size_t colon = s.find(':');
+          std::size_t eol = s.find("\r\n");
+          if (colon != std::string::npos &&
+              eol != std::string::npos)
+          {
+            std::string tmp(s.substr(0, colon));
+
+            if (that.headersLowerCase_)
+            {
+              Toolbox::ToLowerCase(tmp);
+            }
+
+            std::string key = Toolbox::StripSpaces(tmp);
+
+            if (!key.empty())
+            {
+              std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
+              that.answer_.AddHeader(key, value);
+            }
+          }
+
+          return length;
         }
       }
       catch (OrthancException& e)
@@ -294,6 +351,75 @@
         return CURL_READFUNC_ABORT;
       }
     }
+
+    static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
+
+        size_t length = size * nmemb;
+        if (length == 0)
+        {
+          return 0;
+        }
+        else
+        {
+          that.answer_.AddChunk(buffer, length);
+          return length;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
+
+  class HttpClient::DefaultAnswer : public HttpClient::IAnswer
+  {
+  private:
+    ChunkedBuffer   answer_;
+    HttpHeaders*    headers_;
+
+  public:
+    DefaultAnswer() : headers_(NULL)
+    {
+    }
+
+    void SetHeaders(HttpHeaders& headers)
+    {
+      headers_ = &headers;
+      headers_->clear();
+    }
+
+    void FlattenBody(std::string& target)
+    {
+      answer_.Flatten(target);
+    }
+
+    virtual void AddHeader(const std::string& key,
+                           const std::string& value)
+    {
+      if (headers_ != NULL)
+      {
+        (*headers_) [key] = value;
+      }
+    }
+      
+    virtual void AddChunk(const void* data,
+                          size_t size)
+    {
+      answer_.AddChunk(data, size);
+    }
   };
 
 
@@ -404,7 +530,7 @@
     CurlHeaders defaultPostHeaders_;
     CurlHeaders defaultChunkedHeaders_;
     CurlHeaders userHeaders_;
-    CurlRequestChunkedBody chunkedBody_;
+    CurlRequestBody requestBody_;
   };
 
 
@@ -428,23 +554,6 @@
   }
 
 
-  static size_t CurlBodyCallback(void *buffer, size_t size, size_t nmemb, void *payload)
-  {
-    ChunkedBuffer& target = *(static_cast<ChunkedBuffer*>(payload));
-
-    size_t length = size * nmemb;
-    if (length == 0)
-    {
-      return 0;
-    }
-    else
-    {
-      target.AddChunk(buffer, length);
-      return length;
-    }
-  }
-
-
   /*static int CurlDebugCallback(CURL *handle,
                                curl_infotype type,
                                char *data,
@@ -475,52 +584,6 @@
     }*/
 
 
-  struct CurlHeaderParameters
-  {
-    bool lowerCase_;
-    HttpClient::HttpHeaders* headers_;
-  };
-
-
-  static size_t CurlHeaderCallback(void *buffer, size_t size, size_t nmemb, void *payload)
-  {
-    CurlHeaderParameters& parameters = *(static_cast<CurlHeaderParameters*>(payload));
-    assert(parameters.headers_ != NULL);
-
-    size_t length = size * nmemb;
-    if (length == 0)
-    {
-      return 0;
-    }
-    else
-    {
-      std::string s(reinterpret_cast<const char*>(buffer), length);
-      std::size_t colon = s.find(':');
-      std::size_t eol = s.find("\r\n");
-      if (colon != std::string::npos &&
-          eol != std::string::npos)
-      {
-        std::string tmp(s.substr(0, colon));
-
-        if (parameters.lowerCase_)
-        {
-          Toolbox::ToLowerCase(tmp);
-        }
-
-        std::string key = Toolbox::StripSpaces(tmp);
-
-        if (!key.empty())
-        {
-          std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
-          (*parameters.headers_) [key] = value;
-        }
-      }
-
-      return length;
-    }
-  }
-
-
   void HttpClient::Setup()
   {
     pimpl_->defaultPostHeaders_.AddHeader("Expect", "");
@@ -529,7 +592,8 @@
 
     pimpl_->curl_ = curl_easy_init();
 
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlBodyCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
 
@@ -604,21 +668,21 @@
   void HttpClient::SetBody(const std::string& data)
   {
     body_ = data;
-    pimpl_->chunkedBody_.Clear();
+    pimpl_->requestBody_.Clear();
   }
 
 
-  void HttpClient::SetBody(IRequestChunkedBody& body)
+  void HttpClient::SetBody(IRequestBody& body)
   {
     body_.clear();
-    pimpl_->chunkedBody_.SetBody(body);
+    pimpl_->requestBody_.SetBody(body);
   }
 
   
   void HttpClient::ClearBody()
   {
     body_.clear();
-    pimpl_->chunkedBody_.Clear();
+    pimpl_->requestBody_.Clear();
   }
 
 
@@ -658,26 +722,10 @@
   }
 
 
-  bool HttpClient::ApplyInternal(std::string& answerBody,
-                                 HttpHeaders* answerHeaders)
+  bool HttpClient::ApplyInternal(CurlAnswer& answer)
   {
-    answerBody.clear();
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
-
-    CurlHeaderParameters headerParameters;
-
-    if (answerHeaders == NULL)
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, NULL));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, NULL));
-    }
-    else
-    {
-      headerParameters.lowerCase_ = headersToLowerCase_;
-      headerParameters.headers_ = answerHeaders;
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlHeaderCallback));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &headerParameters));
-    }
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer));
 
 #if ORTHANC_ENABLE_SSL == 1
     // Setup HTTPS-related options
@@ -827,10 +875,10 @@
         LOG(INFO) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests";
       }
 
-      if (pimpl_->chunkedBody_.IsValid())
+      if (pimpl_->requestBody_.IsValid())
       {
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestChunkedBody::Callback));
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->chunkedBody_));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_));
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L));
     
@@ -840,7 +888,8 @@
         }
         else if (!pimpl_->userHeaders_.IsChunkedTransfer())
         {
-          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" in streamed POST/PUT requests";
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" "
+                       << "if streaming a chunked body in POST/PUT requests";
         }
       }
       else
@@ -849,6 +898,12 @@
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL));
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0));
 
+        if (pimpl_->userHeaders_.IsChunkedTransfer())
+        {
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set "
+                       << "if streaming a chunked body in POST/PUT requests";
+        }
+
         if (pimpl_->userHeaders_.IsEmpty())
         {
           pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_);
@@ -872,8 +927,7 @@
     CURLcode code;
     long status = 0;
 
-    ChunkedBuffer buffer;
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &buffer));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
 
     if (boost::starts_with(url_, "https://"))
     {
@@ -904,20 +958,42 @@
       lastStatus_ = static_cast<HttpStatus>(status);
     }
 
-    bool success = (status >= 200 && status < 300);
-
-    if (success)
+    if (status >= 200 && status < 300)
     {
-      buffer.Flatten(answerBody);
+      return true;   // Success
     }
     else
     {
-      answerBody.clear();
       LOG(ERROR) << "Error in HTTP request, received HTTP status " << status 
                  << " (" << EnumerationToString(lastStatus_) << ")";
+      return false;
+    }
+  }
+
+
+  bool HttpClient::ApplyInternal(std::string& answerBody,
+                                 HttpHeaders* answerHeaders)
+  {
+    answerBody.clear();
+
+    DefaultAnswer answer;
+
+    if (answerHeaders != NULL)
+    {
+      answer.SetHeaders(*answerHeaders);
     }
 
-    return success;
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (ApplyInternal(wrapper))
+    {
+      answer.FlattenBody(answerBody);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
   }
 
 
@@ -1008,6 +1084,24 @@
   }
 
 
+  bool HttpClient::Apply(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+    return ApplyInternal(wrapper);
+  }
+
+
+  void HttpClient::ApplyAndThrowException(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (!ApplyInternal(wrapper))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
   void HttpClient::ApplyAndThrowException(std::string& answerBody)
   {
     if (!Apply(answerBody))