changeset 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 2f82ef41bf5a
files Core/HttpClient.cpp Core/HttpClient.h Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/OrthancPlugins.h Plugins/Include/orthanc/OrthancCPlugin.h Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Plugins/Samples/Common/OrthancPluginCppWrapper.h
diffstat 7 files changed, 880 insertions(+), 376 deletions(-) [+]
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))
--- a/Core/HttpClient.h	Thu Jun 06 10:35:16 2019 +0200
+++ b/Core/HttpClient.h	Thu Jun 06 16:12:55 2019 +0200
@@ -57,19 +57,35 @@
   public:
     typedef std::map<std::string, std::string>  HttpHeaders;
 
-    class IRequestChunkedBody : public boost::noncopyable
+    class IRequestBody : public boost::noncopyable
     {
     public:
-      virtual ~IRequestChunkedBody()
+      virtual ~IRequestBody()
       {
       }
       
       virtual bool ReadNextChunk(std::string& chunk) = 0;
     };
 
+    class IAnswer : public boost::noncopyable
+    {
+    public:
+      virtual ~IAnswer()
+      {
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value) = 0;
+      
+      virtual void AddChunk(const void* data,
+                            size_t size) = 0;
+    };
+
   private:
     class CurlHeaders;
-    class CurlRequestChunkedBody;
+    class CurlRequestBody;
+    class CurlAnswer;
+    class DefaultAnswer;
     class GlobalParameters;
 
     struct PImpl;
@@ -97,6 +113,8 @@
     void operator= (const HttpClient&);  // Assignment forbidden
     HttpClient(const HttpClient& base);  // Copy forbidden
 
+    bool ApplyInternal(CurlAnswer& answer);
+
     bool ApplyInternal(std::string& answerBody,
                        HttpHeaders* answerHeaders);
 
@@ -158,7 +176,7 @@
       return body_;
     }
 
-    void SetBody(IRequestChunkedBody& body);
+    void SetBody(IRequestBody& body);
 
     void ClearBody();
 
@@ -174,6 +192,8 @@
 
     void ClearHeaders();
 
+    bool Apply(IAnswer& answer);
+
     bool Apply(std::string& answerBody)
     {
       return ApplyInternal(answerBody, NULL);
@@ -295,6 +315,8 @@
 
     static void SetDefaultTimeout(long timeout);
 
+    void ApplyAndThrowException(IAnswer& answer);
+
     void ApplyAndThrowException(std::string& answerBody);
 
     void ApplyAndThrowException(Json::Value& answerBody);
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Jun 06 10:35:16 2019 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Jun 06 16:12:55 2019 +0200
@@ -43,7 +43,6 @@
 #endif
 
 
-#include "../../Core/ChunkedBuffer.h"
 #include "../../Core/Compression/GzipCompressor.h"
 #include "../../Core/Compression/ZlibCompressor.h"
 #include "../../Core/DicomFormat/DicomArray.h"
@@ -356,7 +355,7 @@
       
     public:
       DicomWebBinaryFormatter(const _OrthancPluginEncodeDicomWeb& parameters) :
-        callback_(parameters.callback)
+      callback_(parameters.callback)
       {
       }
       
@@ -422,8 +421,8 @@
 
     public:
       PluginHttpOutput(HttpOutput& output) :
-        output_(output),
-        logDetails_(false)
+      output_(output),
+      logDetails_(false)
       {
       }
 
@@ -520,8 +519,8 @@
 
     public:
       ServerContextLock(PImpl& that) : 
-        lock_(that.contextMutex_),
-        context_(that.context_)
+      lock_(that.contextMutex_),
+      context_(that.context_)
       {
         if (context_ == NULL)
         {
@@ -976,15 +975,15 @@
 
 
 
-  class OrthancPlugins::HttpRequestBody : public HttpClient::IRequestChunkedBody
+  class OrthancPlugins::StreamingHttpRequest : public HttpClient::IRequestBody
   {
   private:
-    const _OrthancPluginHttpClientChunkedBody&  params_;
-    PluginsErrorDictionary&                     errorDictionary_;
+    const _OrthancPluginStreamingHttpClient&  params_;
+    PluginsErrorDictionary&                   errorDictionary_;
 
   public:
-    HttpRequestBody(const _OrthancPluginHttpClientChunkedBody& params,
-                       PluginsErrorDictionary&  errorDictionary) :
+    StreamingHttpRequest(const _OrthancPluginStreamingHttpClient& params,
+                         PluginsErrorDictionary&  errorDictionary) :
       params_(params),
       errorDictionary_(errorDictionary)
     {
@@ -992,23 +991,23 @@
 
     virtual bool ReadNextChunk(std::string& chunk)
     {
-      if (params_.requestBodyIsDone(params_.requestBody))
+      if (params_.requestIsDone(params_.request))
       {
         return false;
       }
       else
       {
-        size_t size = params_.requestBodyChunkSize(params_.requestBody);
+        size_t size = params_.requestChunkSize(params_.request);
 
         chunk.resize(size);
         
         if (size != 0)
         {
-          const void* data = params_.requestBodyChunkData(params_.requestBody);
+          const void* data = params_.requestChunkData(params_.request);
           memcpy(&chunk[0], data, size);
         }
 
-        OrthancPluginErrorCode error = params_.requestBodyNext(params_.requestBody);
+        OrthancPluginErrorCode error = params_.requestNext(params_.request);
         
         if (error != OrthancPluginErrorCode_Success)
         {
@@ -1024,6 +1023,46 @@
   };
 
 
+  class OrthancPlugins::StreamingHttpAnswer : public HttpClient::IAnswer
+  {
+  private:
+    const _OrthancPluginStreamingHttpClient&  params_;
+    PluginsErrorDictionary&                   errorDictionary_;
+
+  public:
+    StreamingHttpAnswer(const _OrthancPluginStreamingHttpClient& params,
+                        PluginsErrorDictionary&  errorDictionary) :
+      params_(params),
+      errorDictionary_(errorDictionary)
+    {
+    }
+
+    virtual void AddHeader(const std::string& key,
+                           const std::string& value)
+    {
+      OrthancPluginErrorCode error = params_.answerAddHeader(params_.answer, key.c_str(), value.c_str());
+        
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        errorDictionary_.LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+      
+    virtual void AddChunk(const void* data,
+                          size_t size)
+    {
+      OrthancPluginErrorCode error = params_.answerAddChunk(params_.answer, data, size);
+        
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        errorDictionary_.LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+  };
+
+
   OrthancPlugins::OrthancPlugins()
   {
     /* Sanity check of the compiler */
@@ -2085,8 +2124,8 @@
   }
 
 
-  static void RunHttpClient(HttpClient& client,
-                            const _OrthancPluginCallHttpClient2& parameters)
+  static void SetupHttpClient(HttpClient& client,
+                              const _OrthancPluginCallHttpClient2& parameters)
   {
     client.SetUrl(parameters.url);
     client.SetConvertHeadersToLowerCase(false);
@@ -2154,6 +2193,18 @@
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
+  }
+
+
+  static void ExecuteHttpClientWithoutStream(uint16_t& httpStatus,
+                                             OrthancPluginMemoryBuffer* answerBody,
+                                             OrthancPluginMemoryBuffer* answerHeaders,
+                                             HttpClient& client)
+  {
+    if (answerBody == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
 
     std::string body;
     HttpClient::HttpHeaders headers;
@@ -2161,7 +2212,7 @@
     bool success = client.Apply(body, headers);
 
     // The HTTP request has succeeded
-    *parameters.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+    httpStatus = static_cast<uint16_t>(client.GetLastStatus());
 
     if (!success)
     {
@@ -2169,7 +2220,7 @@
     }
 
     // Copy the HTTP headers of the answer, if the plugin requested them
-    if (parameters.answerHeaders != NULL)
+    if (answerHeaders != NULL)
     {
       Json::Value json = Json::objectValue;
 
@@ -2180,13 +2231,13 @@
       }
         
       std::string s = json.toStyledString();
-      CopyToMemoryBuffer(*parameters.answerHeaders, s);
+      CopyToMemoryBuffer(*answerHeaders, s);
     }
 
     // Copy the body of the answer if it makes sense
-    if (parameters.method != OrthancPluginHttpMethod_Delete)
+    if (client.GetMethod() != HttpMethod_Delete)
     {
-      CopyToMemoryBuffer(*parameters.answerBody, body);
+      CopyToMemoryBuffer(*answerBody, body);
     }
   }
 
@@ -2194,32 +2245,36 @@
   void OrthancPlugins::CallHttpClient(const void* parameters)
   {
     const _OrthancPluginCallHttpClient& p = *reinterpret_cast<const _OrthancPluginCallHttpClient*>(parameters);
-    
-    _OrthancPluginCallHttpClient2 converted;
-    memset(&converted, 0, sizeof(converted));
-
-    uint16_t httpStatus;
-
-    converted.answerBody = p.target;
-    converted.answerHeaders = NULL;
-    converted.httpStatus = &httpStatus;
-    converted.method = p.method;
-    converted.url = p.url;
-    converted.headersCount = 0;
-    converted.headersKeys = NULL;
-    converted.headersValues = NULL;
-    converted.body = p.body;
-    converted.bodySize = p.bodySize;
-    converted.username = p.username;
-    converted.password = p.password;
-    converted.timeout = 0;  // Use default timeout
-    converted.certificateFile = NULL;
-    converted.certificateKeyFile = NULL;
-    converted.certificateKeyPassword = NULL;
-    converted.pkcs11 = false;
 
     HttpClient client;
-    RunHttpClient(client, converted);
+
+    {    
+      _OrthancPluginCallHttpClient2 converted;
+      memset(&converted, 0, sizeof(converted));
+
+      converted.answerBody = NULL;
+      converted.answerHeaders = NULL;
+      converted.httpStatus = NULL;
+      converted.method = p.method;
+      converted.url = p.url;
+      converted.headersCount = 0;
+      converted.headersKeys = NULL;
+      converted.headersValues = NULL;
+      converted.body = p.body;
+      converted.bodySize = p.bodySize;
+      converted.username = p.username;
+      converted.password = p.password;
+      converted.timeout = 0;  // Use default timeout
+      converted.certificateFile = NULL;
+      converted.certificateKeyFile = NULL;
+      converted.certificateKeyPassword = NULL;
+      converted.pkcs11 = false;
+
+      SetupHttpClient(client, converted);
+    }
+
+    uint16_t status;
+    ExecuteHttpClientWithoutStream(status, p.target, NULL, client);
   }
 
 
@@ -2227,57 +2282,74 @@
   {
     const _OrthancPluginCallHttpClient2& p = *reinterpret_cast<const _OrthancPluginCallHttpClient2*>(parameters);
     
+    if (p.httpStatus == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
     HttpClient client;
 
     if (p.method == OrthancPluginHttpMethod_Post ||
         p.method == OrthancPluginHttpMethod_Put)
     {
-        client.GetBody().assign(p.body, p.bodySize);
+      client.GetBody().assign(p.body, p.bodySize);
     }
     
-    RunHttpClient(client, p);
+    SetupHttpClient(client, p);
+    ExecuteHttpClientWithoutStream(*p.httpStatus, p.answerBody, p.answerHeaders, client);
   }
 
 
-  void OrthancPlugins::HttpClientChunkedBody(const void* parameters)
+  void OrthancPlugins::StreamingHttpClient(const void* parameters)
   {
-    const _OrthancPluginHttpClientChunkedBody& p =
-      *reinterpret_cast<const _OrthancPluginHttpClientChunkedBody*>(parameters);
-    
-    if (p.method != OrthancPluginHttpMethod_Post &&
-        p.method != OrthancPluginHttpMethod_Put)
+    const _OrthancPluginStreamingHttpClient& p =
+      *reinterpret_cast<const _OrthancPluginStreamingHttpClient*>(parameters);
+        
+    if (p.httpStatus == NULL)
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "This plugin service is only allowed for PUT and POST HTTP requests");
+      throw OrthancException(ErrorCode_NullPointer);
     }
 
-    HttpRequestBody body(p, pimpl_->dictionary_);
-
     HttpClient client;
-    client.SetBody(body);
+
+    {
+      _OrthancPluginCallHttpClient2 converted;
+      memset(&converted, 0, sizeof(converted));
+
+      converted.answerBody = NULL;
+      converted.answerHeaders = NULL;
+      converted.httpStatus = NULL;
+      converted.method = p.method;
+      converted.url = p.url;
+      converted.headersCount = p.headersCount;
+      converted.headersKeys = p.headersKeys;
+      converted.headersValues = p.headersValues;
+      converted.body = NULL;
+      converted.bodySize = 0;
+      converted.username = p.username;
+      converted.password = p.password;
+      converted.timeout = p.timeout;
+      converted.certificateFile = p.certificateFile;
+      converted.certificateKeyFile = p.certificateKeyFile;
+      converted.certificateKeyPassword = p.certificateKeyPassword;
+      converted.pkcs11 = p.pkcs11;
+
+      SetupHttpClient(client, converted);
+    }
     
-    _OrthancPluginCallHttpClient2 converted;
-    memset(&converted, 0, sizeof(converted));
-
-    converted.answerBody = p.answerBody;
-    converted.answerHeaders = p.answerHeaders;
-    converted.httpStatus = p.httpStatus;
-    converted.method = p.method;
-    converted.url = p.url;
-    converted.headersCount = p.headersCount;
-    converted.headersKeys = p.headersKeys;
-    converted.headersValues = p.headersValues;
-    converted.body = NULL;
-    converted.bodySize = 0;
-    converted.username = p.username;
-    converted.password = p.password;
-    converted.timeout = p.timeout;
-    converted.certificateFile = p.certificateFile;
-    converted.certificateKeyFile = p.certificateKeyFile;
-    converted.certificateKeyPassword = p.certificateKeyPassword;
-    converted.pkcs11 = p.pkcs11;
-    
-    RunHttpClient(client, converted);
+    StreamingHttpRequest body(p, pimpl_->dictionary_);
+    client.SetBody(body);
+
+    StreamingHttpAnswer answer(p, pimpl_->dictionary_);
+
+    bool success = client.Apply(answer);
+
+    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+
+    if (!success)
+    {
+      HttpClient::ThrowException(client.GetLastStatus());
+    }
   }
 
 
@@ -2959,8 +3031,8 @@
         CallHttpClient2(parameters);
         return true;
 
-      case _OrthancPluginService_HttpClientChunkedBody:
-        HttpClientChunkedBody(parameters);
+      case _OrthancPluginService_StreamingHttpClient:
+        StreamingHttpClient(parameters);
         return true;
 
       case _OrthancPluginService_ConvertPixelFormat:
--- a/Plugins/Engine/OrthancPlugins.h	Thu Jun 06 10:35:16 2019 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Thu Jun 06 16:12:55 2019 +0200
@@ -89,7 +89,8 @@
     class WorklistHandler;
     class FindHandler;
     class MoveHandler;
-    class HttpRequestBody;
+    class StreamingHttpRequest;
+    class StreamingHttpAnswer;
     
     void RegisterRestCallback(const void* parameters,
                               bool lock);
@@ -165,7 +166,7 @@
 
     void CallHttpClient2(const void* parameters);
 
-    void HttpClientChunkedBody(const void* parameters);
+    void StreamingHttpClient(const void* parameters);
 
     void CallPeerApi(const void* parameters);
   
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jun 06 10:35:16 2019 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jun 06 16:12:55 2019 +0200
@@ -429,7 +429,7 @@
     _OrthancPluginService_SetMetricsValue = 31,
     _OrthancPluginService_EncodeDicomWebJson = 32,
     _OrthancPluginService_EncodeDicomWebXml = 33,
-    _OrthancPluginService_HttpClientChunkedBody = 34,   /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_StreamingHttpClient = 34,   /* New in Orthanc 1.5.7 */
     
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -6808,71 +6808,77 @@
 
 
 
-
-
-  typedef uint8_t (*OrthancPluginHttpRequestBodyIsDone) (void* body);
-
-  typedef OrthancPluginErrorCode (*OrthancPluginHttpRequestBodyNext) (void* body);
-
-  typedef const void* (*OrthancPluginHttpRequestBodyGetChunkData) (void* body);
-
-  typedef uint32_t (*OrthancPluginHttpRequestBodyGetChunkSize) (void* body);
+  typedef OrthancPluginErrorCode (*OrthancPluginHttpAnswerStreamAddHeader) (void* answer,
+                                                                            const char* key,
+                                                                            const char* value);
+
+  typedef OrthancPluginErrorCode (*OrthancPluginHttpAnswerStreamAddChunk) (void* answer,
+                                                                           const void* data,
+                                                                           uint32_t size);
+
+  typedef uint8_t (*OrthancPluginHttpRequestStreamIsDone) (void* request);
+
+  typedef OrthancPluginErrorCode (*OrthancPluginHttpRequestStreamNext) (void* request);
+
+  typedef const void* (*OrthancPluginHttpRequestStreamGetChunkData) (void* request);
+
+  typedef uint32_t (*OrthancPluginHttpRequestStreamGetChunkSize) (void* request);
 
   
 
   typedef struct
   {
-    OrthancPluginMemoryBuffer*            answerBody;
-    OrthancPluginMemoryBuffer*            answerHeaders;
-    uint16_t*                             httpStatus;
-    OrthancPluginHttpMethod               method;
-    const char*                           url;
-    uint32_t                              headersCount;
-    const char* const*                    headersKeys;
-    const char* const*                    headersValues;
-    const char*                           username;
-    const char*                           password;
-    uint32_t                              timeout;
-    const char*                           certificateFile;
-    const char*                           certificateKeyFile;
-    const char*                           certificateKeyPassword;
-    uint8_t                               pkcs11;
-    void*                                 requestBody;
-    OrthancPluginHttpRequestBodyIsDone        requestBodyIsDone;
-    OrthancPluginHttpRequestBodyGetChunkData  requestBodyChunkData;
-    OrthancPluginHttpRequestBodyGetChunkSize  requestBodyChunkSize;
-    OrthancPluginHttpRequestBodyNext          requestBodyNext;
-  } _OrthancPluginHttpClientChunkedBody;
-
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClientChunkedBody(
-    OrthancPluginContext*                 context,
-    OrthancPluginMemoryBuffer*            answerBody,
-    OrthancPluginMemoryBuffer*            answerHeaders,
-    uint16_t*                             httpStatus,
-    OrthancPluginHttpMethod               method,
-    const char*                           url,
-    uint32_t                              headersCount,
-    const char* const*                    headersKeys,
-    const char* const*                    headersValues,
-    const char*                           username,
-    const char*                           password,
-    uint32_t                              timeout,
-    const char*                           certificateFile,
-    const char*                           certificateKeyFile,
-    const char*                           certificateKeyPassword,
-    uint8_t                               pkcs11,
-    void*                                 requestBody,
-    OrthancPluginHttpRequestBodyIsDone        requestBodyIsDone,
-    OrthancPluginHttpRequestBodyGetChunkData  requestBodyChunkData,
-    OrthancPluginHttpRequestBodyGetChunkSize  requestBodyChunkSize,
-    OrthancPluginHttpRequestBodyNext          requestBodyNext)
-  {
-    _OrthancPluginHttpClientChunkedBody params;
+    void*                                       answer;
+    OrthancPluginHttpAnswerStreamAddChunk       answerAddChunk;
+    OrthancPluginHttpAnswerStreamAddHeader      answerAddHeader;
+    uint16_t*                                   httpStatus;
+    OrthancPluginHttpMethod                     method;
+    const char*                                 url;
+    uint32_t                                    headersCount;
+    const char* const*                          headersKeys;
+    const char* const*                          headersValues;
+    void*                                       request;
+    OrthancPluginHttpRequestStreamIsDone        requestIsDone;
+    OrthancPluginHttpRequestStreamGetChunkData  requestChunkData;
+    OrthancPluginHttpRequestStreamGetChunkSize  requestChunkSize;
+    OrthancPluginHttpRequestStreamNext          requestNext;
+    const char*                                 username;
+    const char*                                 password;
+    uint32_t                                    timeout;
+    const char*                                 certificateFile;
+    const char*                                 certificateKeyFile;
+    const char*                                 certificateKeyPassword;
+    uint8_t                                     pkcs11;
+  } _OrthancPluginStreamingHttpClient;
+
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStreamingHttpClient(
+    OrthancPluginContext*                       context,
+    void*                                       answer,
+    OrthancPluginHttpAnswerStreamAddChunk       answerAddChunk,
+    OrthancPluginHttpAnswerStreamAddHeader      answerAddHeader,
+    uint16_t*                                   httpStatus,
+    OrthancPluginHttpMethod                     method,
+    const char*                                 url,
+    uint32_t                                    headersCount,
+    const char* const*                          headersKeys,
+    const char* const*                          headersValues,
+    void*                                       request,
+    OrthancPluginHttpRequestStreamIsDone        requestIsDone,
+    OrthancPluginHttpRequestStreamGetChunkData  requestChunkData,
+    OrthancPluginHttpRequestStreamGetChunkSize  requestChunkSize,
+    OrthancPluginHttpRequestStreamNext          requestNext,
+    const char*                                 username,
+    const char*                                 password,
+    uint32_t                                    timeout,
+    const char*                                 certificateFile,
+    const char*                                 certificateKeyFile,
+    const char*                                 certificateKeyPassword,
+    uint8_t                                     pkcs11)
+  {
+    _OrthancPluginStreamingHttpClient params;
     memset(&params, 0, sizeof(params));
 
     /* In common with OrthancPluginHttpClient() */
-    params.answerBody = answerBody;
-    params.answerHeaders = answerHeaders;
     params.httpStatus = httpStatus;
     params.method = method;
     params.url = url;
@@ -6887,14 +6893,17 @@
     params.certificateKeyPassword = certificateKeyPassword;
     params.pkcs11 = pkcs11;
 
-    /* For body stream */
-    params.requestBody = requestBody;
-    params.requestBodyIsDone = requestBodyIsDone;
-    params.requestBodyChunkData = requestBodyChunkData;
-    params.requestBodyChunkSize = requestBodyChunkSize;
-    params.requestBodyNext = requestBodyNext;
-
-    return context->InvokeService(context, _OrthancPluginService_HttpClientChunkedBody, &params);
+    /* For streaming */
+    params.answer = answer;
+    params.answerAddChunk = answerAddChunk;
+    params.answerAddHeader = answerAddHeader;
+    params.request = request;
+    params.requestIsDone = requestIsDone;
+    params.requestChunkData = requestChunkData;
+    params.requestChunkSize = requestChunkSize;
+    params.requestNext = requestNext;
+
+    return context->InvokeService(context, _OrthancPluginService_StreamingHttpClient, &params);
   }
 
 
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Jun 06 10:35:16 2019 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Jun 06 16:12:55 2019 +0200
@@ -2059,22 +2059,21 @@
 
 
 #if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY == 1
-  class HttpClient::RequestChunkedBody : public boost::noncopyable
+  class HttpClient::RequestBodyWrapper : public boost::noncopyable
   {
   private:
-    static RequestChunkedBody& GetObject(void* body)
+    static RequestBodyWrapper& GetObject(void* body)
     {
       assert(body != NULL);
-      return *reinterpret_cast<RequestChunkedBody*>(body);
+      return *reinterpret_cast<RequestBodyWrapper*>(body);
     }
 
-    IRequestChunkedBody&  body_;
+    IRequestBody&  body_;
     bool           done_;
     std::string    chunk_;
 
   public:
-    RequestChunkedBody(IRequestChunkedBody& body) :
+    RequestBodyWrapper(IRequestBody& body) :
       body_(body),
       done_(false)
     {
@@ -2097,7 +2096,7 @@
 
     static OrthancPluginErrorCode Next(void* body)
     {
-      RequestChunkedBody& that = GetObject(body);
+      RequestBodyWrapper& that = GetObject(body);
         
       if (that.done_)
       {
@@ -2121,14 +2120,62 @@
       }
     }    
   };
+
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+  static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer,
+                                                        const char* key,
+                                                        const char* value)
+  {
+    assert(answer != NULL && key != NULL && value != NULL);
+
+    try
+    {
+      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddHeader(key, value);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
 #endif
 
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+  static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer,
+                                                       const void* data,
+                                                       uint32_t size)
+  {
+    assert(answer != NULL);
+
+    try
+    {
+      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddChunk(data, size);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+
   HttpClient::HttpClient() :
     httpStatus_(0),
     method_(OrthancPluginHttpMethod_Get),
     timeout_(0),
     pkcs11_(false),
-    chunkedBody_(NULL)
+    streamingBody_(NULL)
   {
   }
 
@@ -2169,108 +2216,356 @@
   void HttpClient::ClearBody()
   {
     body_.clear();
-    chunkedBody_ = NULL;
+    streamingBody_ = NULL;
   }
 
   
   void HttpClient::SwapBody(std::string& body)
   {
     body_.swap(body);
-    chunkedBody_ = NULL;
+    streamingBody_ = NULL;
   }
 
   
   void HttpClient::SetBody(const std::string& body)
   {
     body_ = body;
-    chunkedBody_ = NULL;
+    streamingBody_ = NULL;
   }
 
   
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY == 1
-  void HttpClient::SetBody(IRequestChunkedBody& body)
+  void HttpClient::SetBody(IRequestBody& body)
   {
     body_.clear();
-    chunkedBody_ = &body;
+    streamingBody_ = &body;
   }
-#endif
-
-  
-  void HttpClient::Execute()
+
+
+  namespace
   {
-    std::vector<const char*> headersKeys;
-    std::vector<const char*> headersValues;
-
-    headersKeys.reserve(headers_.size());
-    headersValues.reserve(headers_.size());
-
-    for (HttpHeaders::const_iterator it = headers_.begin();
-         it != headers_.end(); ++it)
+    class HeadersWrapper : public boost::noncopyable
+    {
+    private:
+      std::vector<const char*>  headersKeys_;
+      std::vector<const char*>  headersValues_;
+
+    public:
+      HeadersWrapper(const HttpClient::HttpHeaders& headers)
+      {
+        headersKeys_.reserve(headers.size());
+        headersValues_.reserve(headers.size());
+
+        for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+        {
+          headersKeys_.push_back(it->first.c_str());
+          headersValues_.push_back(it->second.c_str());
+        }
+      }
+
+      uint32_t GetCount() const
+      {
+        return headersKeys_.size();
+      }
+
+      const char* const* GetKeys() const
+      {
+        return headersKeys_.empty() ? NULL : &headersKeys_[0];
+      }
+
+      const char* const* GetValues() const
+      {
+        return headersValues_.empty() ? NULL : &headersValues_[0];
+      }
+    };
+
+
+    class MemoryRequestBody : public HttpClient::IRequestBody
+    {
+    private:
+      std::string  body_;
+
+    public:
+      MemoryRequestBody(const std::string& body) :
+        body_(body)
+      {
+      }
+
+      virtual bool ReadNextChunk(std::string& chunk)
+      {
+        chunk.swap(body_);
+        return true;
+      }
+    };
+
+
+    // This class mimics Orthanc::ChunkedBuffer
+    class ChunkedBuffer : public boost::noncopyable
     {
-      headersKeys.push_back(it->first.c_str());
-      headersValues.push_back(it->second.c_str());
-    }
-
-    OrthancPluginErrorCode error;
-      
-    if (chunkedBody_ == NULL)
+    private:
+      typedef std::list<std::string*>  Content;
+
+      Content  content_;
+      size_t   size_;
+
+    public:
+      ChunkedBuffer() :
+        size_(0)
+      {
+      }
+
+      ~ChunkedBuffer()
+      {
+        Clear();
+      }
+
+      void Clear()
+      {
+        for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+        {
+          assert(*it != NULL);
+          delete *it;
+        }
+
+        content_.clear();
+      }
+
+      void Flatten(std::string& target) const
+      {
+        target.resize(size_);
+
+        size_t pos = 0;
+
+        for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+        {
+          assert(*it != NULL);
+          size_t s = (*it)->size();
+
+          if (s != 0)
+          {
+            memcpy(&target[pos], (*it)->c_str(), s);
+            pos += s;
+          }
+        }
+
+        assert(size_ == 0 ||
+               pos == target.size());
+      }
+
+      void AddChunk(const void* data,
+                    size_t size)
+      {
+        content_.push_back(new std::string(reinterpret_cast<const char*>(data), size));
+        size_ += size;
+      }
+
+      void AddChunk(const std::string& chunk)
+      {
+        content_.push_back(new std::string(chunk));
+        size_ += chunk.size();
+      }
+    };
+
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+    class MemoryAnswer : public HttpClient::IAnswer
     {
-      error = OrthancPluginHttpClient(
-        GetGlobalContext(),
-        *answerBody_,
-        *answerHeaders_,
-        &httpStatus_,
-        method_,
-        url_.c_str(),
-        headersKeys.size(),
-        headersKeys.empty() ? NULL : &headersKeys[0],
-        headersValues.empty() ? NULL : &headersValues[0],
-        body_.empty() ? NULL : body_.c_str(),
-        body_.size(),
-        username_.empty() ? NULL : username_.c_str(),
-        password_.empty() ? NULL : password_.c_str(),
-        timeout_,
-        certificateFile_.empty() ? NULL : certificateFile_.c_str(),
-        certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
-        certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
-        pkcs11_ ? 1 : 0);
+    private:
+      HttpClient::HttpHeaders  headers_;
+      ChunkedBuffer            body_;
+
+    public:
+      const HttpClient::HttpHeaders& GetHeaders() const
+      {
+        return headers_;
+      }
+
+      const ChunkedBuffer& GetBody() const
+      {
+        return body_;
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value)
+      {
+        headers_[key] = value;
+      }
+
+      virtual void AddChunk(const void* data,
+                            size_t size)
+      {
+        body_.AddChunk(data, size);
+      }
+    };
+#endif
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+  void HttpClient::ExecuteWithStream(uint16_t& httpStatus,
+                                     IAnswer& answer,
+                                     IRequestBody& body) const
+  {
+    std::auto_ptr<HeadersWrapper> h;
+
+    // Automatically set the "Transfer-Encoding" header if absent
+    if (headers_.find("Transfer-Encoding") == headers_.end())
+    {
+      HttpHeaders tmp = headers_;
+      tmp["Transfer-Encoding"] = "chunked";
+      h.reset(new HeadersWrapper(tmp));
     }
     else
     {
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY != 1
-      error = OrthancPluginErrorCode_InternalError;
-#else
-      RequestChunkedBody wrapper(*chunkedBody_);
+      h.reset(new HeadersWrapper(headers_));
+    }
+
+    RequestBodyWrapper request(body);
         
-      error = OrthancPluginHttpClientChunkedBody(
-        GetGlobalContext(),
-        *answerBody_,
-        *answerHeaders_,
-        &httpStatus_,
-        method_,
-        url_.c_str(),
-        headersKeys.size(),
-        headersKeys.empty() ? NULL : &headersKeys[0],
-        headersValues.empty() ? NULL : &headersValues[0],
-        username_.empty() ? NULL : username_.c_str(),
-        password_.empty() ? NULL : password_.c_str(),
-        timeout_,
-        certificateFile_.empty() ? NULL : certificateFile_.c_str(),
-        certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
-        certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
-        pkcs11_ ? 1 : 0,
-        &wrapper,
-        RequestChunkedBody::IsDone,
-        RequestChunkedBody::GetChunkData,
-        RequestChunkedBody::GetChunkSize,
-        RequestChunkedBody::Next);
-#endif
-    }
+    OrthancPluginErrorCode error = OrthancPluginStreamingHttpClient(
+      GetGlobalContext(),
+      &answer,
+      AnswerAddChunkCallback,
+      AnswerAddHeaderCallback,
+      &httpStatus,
+      method_,
+      url_.c_str(),
+      h->GetCount(),
+      h->GetKeys(),
+      h->GetValues(),
+      &request,
+      RequestBodyWrapper::IsDone,
+      RequestBodyWrapper::GetChunkData,
+      RequestBodyWrapper::GetChunkSize,
+      RequestBodyWrapper::Next,
+      username_.empty() ? NULL : username_.c_str(),
+      password_.empty() ? NULL : password_.c_str(),
+      timeout_,
+      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
+      pkcs11_ ? 1 : 0);
 
     if (error != OrthancPluginErrorCode_Success)
     {
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
     }
   }
+#endif    
+
+
+  void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
+                                        HttpHeaders& answerHeaders,
+                                        std::string& answerBody,
+                                        const std::string& body) const
+  {
+    HeadersWrapper headers(headers_);
+
+    MemoryBuffer answerBodyBuffer, answerHeadersBuffer;
+
+    OrthancPluginErrorCode error = OrthancPluginHttpClient(
+      GetGlobalContext(),
+      *answerBodyBuffer,
+      *answerHeadersBuffer,
+      &httpStatus,
+      method_,
+      url_.c_str(),
+      headers.GetCount(),
+      headers.GetKeys(),
+      headers.GetValues(),
+      body.empty() ? NULL : body.c_str(),
+      body.size(),
+      username_.empty() ? NULL : username_.c_str(),
+      password_.empty() ? NULL : password_.c_str(),
+      timeout_,
+      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
+      pkcs11_ ? 1 : 0);
+
+    if (error != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+    }
+
+    Json::Value v;
+    answerHeadersBuffer.ToJson(v);
+
+    if (v.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Value::Members members = v.getMemberNames();
+    answerHeaders.clear();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& h = v[members[i]];
+      if (h.type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+      else
+      {
+        answerHeaders[members[i]] = h.asString();
+      }
+    }
+
+    answerBodyBuffer.ToString(answerBody);
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+  void HttpClient::Execute(IAnswer& answer)
+  {
+    if (streamingBody_ != NULL)
+    {
+      ExecuteWithStream(httpStatus_, answer, *streamingBody_);
+    }
+    else
+    {
+      MemoryRequestBody wrapper(body_);
+      ExecuteWithStream(httpStatus_, answer, wrapper);
+    }
+  }
+#endif
+
+
+  void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
+                           std::string& answerBody /* out */)
+  {
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+    MemoryAnswer answer;
+    Execute(answer);
+    answerHeaders = answer.GetHeaders();
+    answer.GetBody().Flatten(answerBody);
+
+#else
+    // Compatibility mode for Orthanc SDK <= 1.5.6. This results in
+    // higher memory usage (all chunks from the body request are sent
+    // at once)
+
+    if (streamingBody_ != NULL)
+    {
+      ChunkedBuffer buffer;
+      
+      std::string chunk;
+      while (streamingBody_->ReadNextChunk(chunk))
+      {
+        buffer.AddChunk(chunk);
+      }
+
+      std::string body;
+      buffer.Flatten(body);
+
+      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, body);
+    }
+    else
+    {
+      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, body_);
+    }
+#endif
+  }
+
 #endif  /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */
 }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Jun 06 10:35:16 2019 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Jun 06 16:12:55 2019 +0200
@@ -92,9 +92,9 @@
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
-#  define HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY  1
+#  define HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT  1
 #else
-#  define HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY  0
+#  define HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT  0
 #endif
 
 
@@ -789,24 +789,38 @@
   class HttpClient : public boost::noncopyable
   {
   public:
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY == 1
-    class IRequestChunkedBody : public boost::noncopyable
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
+    class IRequestBody : public boost::noncopyable
     {
     public:
-      virtual ~IRequestChunkedBody()
+      virtual ~IRequestBody()
       {
       }
 
       virtual bool ReadNextChunk(std::string& chunk) = 0;
     };
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+    class IAnswer : public boost::noncopyable
+    {
+    public:
+      virtual ~IAnswer()
+      {
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value) = 0;
+
+      virtual void AddChunk(const void* data,
+                            size_t size) = 0;
+    };
 #endif
-  
+
 
   private:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-    
-    MemoryBuffer             answerBody_;
-    MemoryBuffer             answerHeaders_;
+    class RequestBodyWrapper;
+
     uint16_t                 httpStatus_;
     OrthancPluginHttpMethod  method_;
     std::string              url_;
@@ -819,28 +833,22 @@
     std::string              certificateKeyPassword_;
     bool                     pkcs11_;
     std::string              body_;
+    IRequestBody*            streamingBody_;    
 
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY == 1
-    class RequestChunkedBody;
-    IRequestChunkedBody*     chunkedBody_;
-#else
-    // Dummy variable for backward compatibility
-    void*                    chunkedBody_;
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+    void ExecuteWithStream(uint16_t& httpStatus,  // out
+                           IAnswer& answer,       // out
+                           IRequestBody& body) const;
 #endif
 
+    void ExecuteWithoutStream(uint16_t& httpStatus,        // out
+                              HttpHeaders& answerHeaders,  // out
+                              std::string& answerBody,     // out
+                              const std::string& body) const;
+    
   public:
     HttpClient();
 
-    const MemoryBuffer& GetAnswerBody() const
-    {
-      return answerBody_;
-    }
-
-    const MemoryBuffer& GetAnswerHeaders() const
-    {
-      return answerHeaders_;
-    }
-
     uint16_t GetHttpStatus() const
     {
       return httpStatus_;
@@ -894,11 +902,14 @@
 
     void SetBody(const std::string& body);
 
-#if HAS_ORTHANC_PLUGIN_HTTP_CHUNKED_BODY == 1
-    void SetBody(IRequestChunkedBody& body);
+    void SetBody(IRequestBody& body);
+
+#if HAS_ORTHANC_PLUGIN_STREAMING_HTTP_CLIENT == 1
+    void Execute(IAnswer& answer);
 #endif
 
-    void Execute();
+    void Execute(HttpHeaders& answerHeaders /* out */,
+                 std::string& answerBody /* out */);
   };
 #endif
 }