mirror of
https://github.com/BlackMATov/curly.hpp.git
synced 2025-12-14 20:11:29 +07:00
@@ -29,9 +29,9 @@
|
||||
|
||||
- Custom headers
|
||||
- Asynchronous requests
|
||||
- Different types of timeouts
|
||||
- PUT, GET, HEAD, POST methods
|
||||
- Custom uploading and downloading streams
|
||||
- Connection and last server response timeouts
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class exception : public std::runtime_error {
|
||||
class exception final : public std::runtime_error {
|
||||
public:
|
||||
explicit exception(const char* what);
|
||||
explicit exception(const std::string& what);
|
||||
};
|
||||
|
||||
struct icase_string_compare {
|
||||
struct icase_string_compare final {
|
||||
using is_transparent = void;
|
||||
bool operator()(
|
||||
std::string_view l,
|
||||
@@ -67,7 +67,7 @@ namespace curly_hpp
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class content_t {
|
||||
class content_t final {
|
||||
public:
|
||||
content_t() = default;
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace curly_hpp
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class response {
|
||||
class response final {
|
||||
public:
|
||||
response() = default;
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace curly_hpp
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class request {
|
||||
class request final {
|
||||
public:
|
||||
enum class statuses {
|
||||
done,
|
||||
@@ -146,7 +146,7 @@ namespace curly_hpp
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class request_builder {
|
||||
class request_builder final {
|
||||
public:
|
||||
request_builder() = default;
|
||||
|
||||
@@ -167,6 +167,7 @@ namespace curly_hpp
|
||||
request_builder& verbose(bool v) noexcept;
|
||||
request_builder& verification(bool v) noexcept;
|
||||
request_builder& redirections(std::uint32_t r) noexcept;
|
||||
request_builder& request_timeout(time_sec_t t) noexcept;
|
||||
request_builder& response_timeout(time_sec_t t) noexcept;
|
||||
request_builder& connection_timeout(time_sec_t t) noexcept;
|
||||
|
||||
@@ -182,6 +183,7 @@ namespace curly_hpp
|
||||
bool verbose() const noexcept;
|
||||
bool verification() const noexcept;
|
||||
std::uint32_t redirections() const noexcept;
|
||||
time_sec_t request_timeout() const noexcept;
|
||||
time_sec_t response_timeout() const noexcept;
|
||||
time_sec_t connection_timeout() const noexcept;
|
||||
|
||||
@@ -198,12 +200,14 @@ namespace curly_hpp
|
||||
|
||||
template < typename Uploader, typename... Args >
|
||||
request_builder& uploader(Args&&... args) {
|
||||
return uploader(std::make_unique<Uploader>(std::forward<Args>(args)...));
|
||||
return uploader(std::make_unique<Uploader>(
|
||||
std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template < typename Downloader, typename... Args >
|
||||
request_builder& downloader(Args&&... args) {
|
||||
return downloader(std::make_unique<Downloader>(std::forward<Args>(args)...));
|
||||
return downloader(std::make_unique<Downloader>(
|
||||
std::forward<Args>(args)...));
|
||||
}
|
||||
private:
|
||||
std::string url_;
|
||||
@@ -212,6 +216,7 @@ namespace curly_hpp
|
||||
bool verbose_{false};
|
||||
bool verification_{false};
|
||||
std::uint32_t redirections_{10u};
|
||||
time_sec_t request_timeout_{~0u};
|
||||
time_sec_t response_timeout_{60u};
|
||||
time_sec_t connection_timeout_{20u};
|
||||
private:
|
||||
@@ -223,7 +228,7 @@ namespace curly_hpp
|
||||
|
||||
namespace curly_hpp
|
||||
{
|
||||
class auto_performer {
|
||||
class auto_performer final {
|
||||
public:
|
||||
auto_performer();
|
||||
~auto_performer() noexcept;
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace
|
||||
return {result, &curl_slist_free_all};
|
||||
}
|
||||
|
||||
class default_uploader : public upload_handler {
|
||||
class default_uploader final : public upload_handler {
|
||||
public:
|
||||
using data_t = std::vector<char>;
|
||||
|
||||
@@ -97,11 +97,11 @@ namespace
|
||||
}
|
||||
private:
|
||||
const data_t& data_;
|
||||
std::size_t uploaded_{0};
|
||||
std::mutex& mutex_;
|
||||
std::size_t uploaded_{0};
|
||||
};
|
||||
|
||||
class default_downloader : public download_handler {
|
||||
class default_downloader final : public download_handler {
|
||||
public:
|
||||
using data_t = std::vector<char>;
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace
|
||||
{
|
||||
using namespace curly_hpp;
|
||||
|
||||
class curl_state {
|
||||
class curl_state final {
|
||||
public:
|
||||
template < typename F >
|
||||
static std::invoke_result_t<F, CURLM*> with(F&& f)
|
||||
@@ -156,11 +156,8 @@ namespace
|
||||
|
||||
~curl_state() noexcept {
|
||||
std::lock_guard<std::mutex> guard(mutex_);
|
||||
if ( curlm_ ) {
|
||||
curl_multi_cleanup(curlm_);
|
||||
curl_global_cleanup();
|
||||
curlm_ = nullptr;
|
||||
}
|
||||
curl_multi_cleanup(curlm_);
|
||||
curl_global_cleanup();
|
||||
}
|
||||
private:
|
||||
CURLM* curlm_{nullptr};
|
||||
@@ -344,6 +341,9 @@ namespace curly_hpp
|
||||
curl_easy_setopt(curlh_.get(), CURLOPT_FOLLOWLOCATION, 0l);
|
||||
}
|
||||
|
||||
curl_easy_setopt(curlh_.get(), CURLOPT_TIMEOUT,
|
||||
static_cast<long>(std::max(time_sec_t(1), rb.request_timeout()).count()));
|
||||
|
||||
curl_easy_setopt(curlh_.get(), CURLOPT_CONNECTTIMEOUT,
|
||||
static_cast<long>(std::max(time_sec_t(1), rb.connection_timeout()).count()));
|
||||
|
||||
@@ -393,9 +393,19 @@ namespace curly_hpp
|
||||
return false;
|
||||
}
|
||||
|
||||
status_ = (err == CURLE_OPERATION_TIMEDOUT)
|
||||
? statuses::timeout
|
||||
: statuses::failed;
|
||||
switch ( err ) {
|
||||
case CURLE_OPERATION_TIMEDOUT:
|
||||
status_ = statuses::timeout;
|
||||
break;
|
||||
case CURLE_READ_ERROR:
|
||||
case CURLE_WRITE_ERROR:
|
||||
case CURLE_ABORTED_BY_CALLBACK:
|
||||
status_ = statuses::canceled;
|
||||
break;
|
||||
default:
|
||||
status_ = statuses::failed;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
error_ = curl_easy_strerror(err);
|
||||
@@ -632,6 +642,11 @@ namespace curly_hpp
|
||||
return *this;
|
||||
}
|
||||
|
||||
request_builder& request_builder::request_timeout(time_sec_t t) noexcept {
|
||||
request_timeout_ = t;
|
||||
return *this;
|
||||
}
|
||||
|
||||
request_builder& request_builder::response_timeout(time_sec_t t) noexcept {
|
||||
response_timeout_ = t;
|
||||
return *this;
|
||||
@@ -686,6 +701,10 @@ namespace curly_hpp
|
||||
return redirections_;
|
||||
}
|
||||
|
||||
time_sec_t request_builder::request_timeout() const noexcept {
|
||||
return request_timeout_;
|
||||
}
|
||||
|
||||
time_sec_t request_builder::response_timeout() const noexcept {
|
||||
return response_timeout_;
|
||||
}
|
||||
@@ -792,8 +811,11 @@ namespace curly_hpp
|
||||
void perform() {
|
||||
curl_state::with([](CURLM* curlm){
|
||||
int running_handles = 0;
|
||||
curl_multi_perform(curlm, &running_handles);
|
||||
if ( !running_handles || static_cast<std::size_t>(running_handles) != handles.size() ) {
|
||||
if ( CURLM_OK != curl_multi_perform(curlm, &running_handles) ) {
|
||||
throw exception("curly_hpp: failed to curl_multi_perform");
|
||||
}
|
||||
|
||||
if ( static_cast<std::size_t>(running_handles) != handles.size() ) {
|
||||
while ( true ) {
|
||||
int msgs_in_queue = 0;
|
||||
CURLMsg* msg = curl_multi_info_read(curlm, &msgs_in_queue);
|
||||
@@ -833,7 +855,10 @@ namespace curly_hpp
|
||||
|
||||
void wait_activity(time_ms_t ms) {
|
||||
curl_state::with([ms](CURLM* curlm){
|
||||
curl_multi_wait(curlm, nullptr, 0, static_cast<int>(ms.count()), nullptr);
|
||||
const int timeout_ms = static_cast<int>(ms.count());
|
||||
if ( CURLM_OK != curl_multi_wait(curlm, nullptr, 0, timeout_ms, nullptr) ) {
|
||||
throw exception("curly_hpp: failed to curl_multi_wait");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,4 @@
|
||||
******************************************************************************/
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#define CATCH_CONFIG_FAST_COMPILE
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
* Copyright (C) 2019, by Matvey Cherevko (blackmatov@gmail.com)
|
||||
******************************************************************************/
|
||||
|
||||
#define CATCH_CONFIG_FAST_COMPILE
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include <iostream>
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
@@ -29,35 +29,36 @@ namespace
|
||||
return d;
|
||||
}
|
||||
|
||||
class verbose_uploader : public net::upload_handler {
|
||||
class canceled_uploader : public net::upload_handler {
|
||||
public:
|
||||
verbose_uploader() = default;
|
||||
canceled_uploader() = default;
|
||||
|
||||
std::size_t size() const override {
|
||||
return 0;
|
||||
return 10;
|
||||
}
|
||||
|
||||
std::size_t read(char* dst, std::size_t size) override {
|
||||
(void)dst;
|
||||
std::cout << "---------- ** UPLOAD (" << size << ") ** ---------- " << std::endl;
|
||||
return size;
|
||||
(void)size;
|
||||
throw std::exception();
|
||||
}
|
||||
};
|
||||
|
||||
class verbose_downloader : public net::download_handler {
|
||||
class canceled_downloader : public net::download_handler {
|
||||
public:
|
||||
verbose_downloader() = default;
|
||||
canceled_downloader() = default;
|
||||
|
||||
std::size_t write(const char* src, std::size_t size) override {
|
||||
(void)src;
|
||||
std::cout << "---------- ** DOWNLOAD (" << size << ") ** ---------- " << std::endl;
|
||||
return size;
|
||||
(void)size;
|
||||
throw std::exception();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("curly") {
|
||||
net::auto_performer performer;
|
||||
performer.wait_activity(net::time_ms_t(10));
|
||||
|
||||
SECTION("wait") {
|
||||
auto req = net::request_builder("https://httpbin.org/delay/1").send();
|
||||
@@ -229,11 +230,13 @@ TEST_CASE("curly") {
|
||||
.url("https://httpbin.org/headers")
|
||||
.header("Custom-Header-1", "custom_header_value_1")
|
||||
.header("Custom-Header-2", "custom header value 2")
|
||||
.header("Custom-Header-3", std::string())
|
||||
.send();
|
||||
const auto resp = req.get();
|
||||
const auto content_j = json_parse(resp.content.as_string_view());
|
||||
REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1");
|
||||
REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2");
|
||||
REQUIRE(content_j["headers"]["Custom-Header-3"] == "");
|
||||
}
|
||||
|
||||
SECTION("response_inspection") {
|
||||
@@ -253,7 +256,7 @@ TEST_CASE("curly") {
|
||||
.method(net::methods::post)
|
||||
.send();
|
||||
const auto resp = req.get();
|
||||
const auto content_j = json_parse(resp.content.as_string_view());
|
||||
const auto content_j = json_parse(resp.content.as_string_copy());
|
||||
REQUIRE(content_j["hello"] == "world");
|
||||
REQUIRE(content_j["world"] == "hello");
|
||||
}
|
||||
@@ -269,20 +272,34 @@ TEST_CASE("curly") {
|
||||
REQUIRE(req.error().empty());
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
auto req0 = net::request_builder()
|
||||
.url("https://httpbin.org/delay/10")
|
||||
.request_timeout(net::time_sec_t(0))
|
||||
.send();
|
||||
REQUIRE(req0.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req0.error().empty());
|
||||
|
||||
auto req1 = net::request_builder()
|
||||
.url("https://httpbin.org/delay/10")
|
||||
.response_timeout(net::time_sec_t(0))
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req.error().empty());
|
||||
REQUIRE(req1.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req1.error().empty());
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
auto req0 = net::request_builder()
|
||||
.url("https://httpbin.org/delay/10")
|
||||
.request_timeout(net::time_sec_t(1))
|
||||
.send();
|
||||
REQUIRE(req0.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req0.error().empty());
|
||||
|
||||
auto req1 = net::request_builder()
|
||||
.url("https://httpbin.org/delay/10")
|
||||
.response_timeout(net::time_sec_t(1))
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req.error().empty());
|
||||
REQUIRE(req1.wait() == net::request::statuses::timeout);
|
||||
REQUIRE_FALSE(req1.error().empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +313,9 @@ TEST_CASE("curly") {
|
||||
REQUIRE(resp.headers.count("Content-Type"));
|
||||
REQUIRE(resp.headers.at("Content-Type") == "image/png");
|
||||
REQUIRE(untests::png_data_length == resp.content.size());
|
||||
REQUIRE(!std::memcmp(resp.content.data().data(), untests::png_data, untests::png_data_length));
|
||||
REQUIRE(!std::memcmp(
|
||||
std::move(resp.content).data().data(),
|
||||
untests::png_data, untests::png_data_length));
|
||||
}
|
||||
{
|
||||
auto resp = net::request_builder()
|
||||
@@ -307,31 +326,69 @@ TEST_CASE("curly") {
|
||||
REQUIRE(resp.headers.count("Content-Type"));
|
||||
REQUIRE(resp.headers.at("Content-Type") == "image/jpeg");
|
||||
REQUIRE(untests::jpeg_data_length == resp.content.size());
|
||||
REQUIRE(!std::memcmp(resp.content.data().data(), untests::jpeg_data, untests::jpeg_data_length));
|
||||
REQUIRE(!std::memcmp(
|
||||
std::as_const(resp.content).data().data(),
|
||||
untests::jpeg_data, untests::jpeg_data_length));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("redirects") {
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/absolute-redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/relative-redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/absolute-redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/relative-redirect/2")
|
||||
.method(net::methods::get)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/3")
|
||||
.method(net::methods::get)
|
||||
.redirections(0)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 302u);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/3")
|
||||
.method(net::methods::get)
|
||||
.redirections(1)
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::failed);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/3")
|
||||
.method(net::methods::get)
|
||||
.redirections(2)
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::failed);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder()
|
||||
.url("https://httpbin.org/redirect/3")
|
||||
.method(net::methods::get)
|
||||
.redirections(3)
|
||||
.send();
|
||||
REQUIRE(req.get().code() == 200u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +478,25 @@ TEST_CASE("curly") {
|
||||
REQUIRE(req3.wait() == net::request::statuses::done);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("canceled_handlers") {
|
||||
{
|
||||
auto req = net::request_builder("https://httpbin.org/anything")
|
||||
.verbose(true)
|
||||
.method(net::methods::post)
|
||||
.uploader<canceled_uploader>()
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::canceled);
|
||||
}
|
||||
{
|
||||
auto req = net::request_builder("https://httpbin.org/anything")
|
||||
.verbose(true)
|
||||
.method(net::methods::get)
|
||||
.downloader<canceled_downloader>()
|
||||
.send();
|
||||
REQUIRE(req.wait() == net::request::statuses::canceled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("curly_examples") {
|
||||
|
||||
Reference in New Issue
Block a user