diff --git a/README.md b/README.md index cbb0bb9..efbd86f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index f693a60..e911ab5 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -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(std::forward(args)...)); + return uploader(std::make_unique( + std::forward(args)...)); } template < typename Downloader, typename... Args > request_builder& downloader(Args&&... args) { - return downloader(std::make_unique(std::forward(args)...)); + return downloader(std::make_unique( + std::forward(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; diff --git a/sources/curly.cpp b/sources/curly.cpp index 84f6a01..dd05e3f 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -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; @@ -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; @@ -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 with(F&& f) @@ -156,11 +156,8 @@ namespace ~curl_state() noexcept { std::lock_guard 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(std::max(time_sec_t(1), rb.request_timeout()).count())); + curl_easy_setopt(curlh_.get(), CURLOPT_CONNECTTIMEOUT, static_cast(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(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(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(ms.count()), nullptr); + const int timeout_ms = static_cast(ms.count()); + if ( CURLM_OK != curl_multi_wait(curlm, nullptr, 0, timeout_ms, nullptr) ) { + throw exception("curly_hpp: failed to curl_multi_wait"); + } }); } } diff --git a/untests/catch_main.cpp b/untests/catch_main.cpp index 0259235..6b9ad68 100644 --- a/untests/catch_main.cpp +++ b/untests/catch_main.cpp @@ -5,5 +5,4 @@ ******************************************************************************/ #define CATCH_CONFIG_MAIN -#define CATCH_CONFIG_FAST_COMPILE #include diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index 6bf26d4..764871a 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -4,10 +4,10 @@ * Copyright (C) 2019, by Matvey Cherevko (blackmatov@gmail.com) ******************************************************************************/ -#define CATCH_CONFIG_FAST_COMPILE #include #include +#include #include #include @@ -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() + .send(); + REQUIRE(req.wait() == net::request::statuses::canceled); + } + { + auto req = net::request_builder("https://httpbin.org/anything") + .verbose(true) + .method(net::methods::get) + .downloader() + .send(); + REQUIRE(req.wait() == net::request::statuses::canceled); + } + } } TEST_CASE("curly_examples") {