diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c10386fc8..1f4d14325 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -15,7 +15,7 @@ jobs: package-server-redis-released: ${{ steps.release.outputs['libs/server-sdk-redis-source--release_created'] }} package-server-redis-tag: ${{ steps.release.outputs['libs/server-sdk-redis-source--tag_name'] }} steps: - - uses: google-github-actions/release-please-action@v4 + - uses: googleapis/release-please-action@v4 id: release with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9cbc31134..d1820b6f2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,8 +1,8 @@ { - "libs/client-sdk": "3.6.1", - "libs/server-sent-events": "0.5.0", + "libs/client-sdk": "3.6.2", + "libs/server-sent-events": "0.5.1", "libs/common": "1.7.0", "libs/internal": "0.8.1", - "libs/server-sdk": "3.5.1", - "libs/server-sdk-redis-source": "2.1.9" + "libs/server-sdk": "3.5.2", + "libs/server-sdk-redis-source": "2.1.10" } diff --git a/README.md b/README.md index 3766d0741..3e866fe13 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ Various CMake options are available to customize the client/server SDK builds. **Note:** _if building the SDKs as shared libraries, then unit tests won't be able to link correctly since the SDK's C++ symbols aren't exposed. To run unit tests, build a static library._ +> [!WARNING] +> When building shared libraries C++ symbols are not exported, only the C API will be exported. This is because C++ does not have a stable ABI. + Basic usage example: ```bash diff --git a/contract-tests/sse-contract-tests/src/event_outbox.cpp b/contract-tests/sse-contract-tests/src/event_outbox.cpp index 2f09615c3..05b7e07ed 100644 --- a/contract-tests/sse-contract-tests/src/event_outbox.cpp +++ b/contract-tests/sse-contract-tests/src/event_outbox.cpp @@ -1,10 +1,12 @@ #include "event_outbox.hpp" #include "definitions.hpp" +#include #include -#include #include +#include + // Check the outbox at this interval. Normally a flush is triggered for // every event; this is just a failsafe in case a flush is happening // concurrently. @@ -78,22 +80,8 @@ EventOutbox::RequestType EventOutbox::build_request( json = EventMessage{"event", Event{std::move(arg)}}; } } else if constexpr (std::is_same_v) { - using launchdarkly::sse::Error; - auto msg = ErrorMessage{"error"}; - switch (arg) { - case Error::NoContent: - msg.comment = "no content"; - break; - case Error::InvalidRedirectLocation: - msg.comment = "invalid redirect location"; - break; - case Error::UnrecoverableClientError: - msg.comment = "unrecoverable client error"; - case Error::ReadTimeout: - msg.comment = "read timeout"; - default: - msg.comment = "unspecified error"; - } + auto msg = ErrorMessage{"error", + launchdarkly::sse::ErrorToString(arg)}; json = msg; } }, diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index ddb6785b6..15a5288ad 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [3.6.2](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.6.1...launchdarkly-cpp-client-v3.6.2) (2024-07-15) + + +### Bug Fixes + +* more helpful error messages for streaming connection failures ([#419](https://github.com/launchdarkly/cpp-sdks/issues/419)) ([6bd21ba](https://github.com/launchdarkly/cpp-sdks/commit/6bd21ba1eafb5f19275935e1f62f7304d4dc69f5)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-sse-client bumped from 0.5.0 to 0.5.1 + ## [3.6.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.6.0...launchdarkly-cpp-client-v3.6.1) (2024-06-11) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 343fc3f25..f0f3ca0c8 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.6.1 # {x-release-please-version} + VERSION 3.6.2 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index 8f72e8de3..dd0a2b46c 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -18,6 +18,10 @@ namespace launchdarkly::client_side { /** * Interface for the standard SDK client methods and properties. + * + * After an SDK instance is created it should be started with the StartAsync + * method. Subsequently if a change of context is required the IdentifyAsync + * method should be used. */ class IClient { public: @@ -28,7 +32,11 @@ class IClient { /** Connects the client to LaunchDarkly's flag delivery endpoints. * - * StartAsync must be called to obtain fresh flag data. + * StartAsync must be called once for the SDK to start receiving flag data. + * StartAsync only needs to be called one time for the lifetime of the SDK + * client. + * To change the context associated with evaluations please call the + * IdentifyAsync method. * * The returned future will resolve to true or false based on the logic * outlined on @ref Initialized. @@ -42,6 +50,10 @@ class IClient { * * While the client is connecting asynchronously, it is safe to call * variation methods, which will return application-defined default values. + * + * The client will always continue to attempt to connect asynchronously + * after being started unless it encounters an unrecoverable error. The + * returned promise timing out does not affect this behavior. */ virtual std::future StartAsync() = 0; @@ -333,7 +345,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.6.1"; // {x-release-please-version} + "3.6.2"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 3cc4c588d..d0c5c51d2 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.6.1", + "version": "3.6.2", "private": true, "dependencies": { "launchdarkly-cpp-internal": "0.8.1", "launchdarkly-cpp-common": "1.7.0", - "launchdarkly-cpp-sse-client": "0.5.0" + "launchdarkly-cpp-sse-client": "0.5.1" } } diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index 39c559252..d26370327 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -19,21 +19,6 @@ namespace launchdarkly::client_side::data_sources { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL."; -static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { - switch (error) { - case sse::Error::NoContent: - return "server responded 204 (No Content), will not attempt to " - "reconnect"; - case sse::Error::InvalidRedirectLocation: - return "server responded with an invalid redirection"; - case sse::Error::UnrecoverableClientError: - return "unrecoverable client-side error"; - case sse::Error::ReadTimeout: - return "read timeout reached"; - } - launchdarkly::detail::unreachable(); -} - StreamingDataSource::StreamingDataSource( config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::DataSourceConfig const& @@ -155,12 +140,12 @@ void StreamingDataSource::Start() { client_builder.errors([weak_self](auto error) { if (auto self = weak_self.lock()) { - auto error_string = DataSourceErrorToString(error); + auto error_string = launchdarkly::sse::ErrorToString(error); LD_LOG(self->logger_, LogLevel::kError) << error_string; self->status_manager_.SetState( DataSourceStatus::DataSourceState::kShutdown, DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, - error_string); + std::move(error_string)); } }); diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index 286bf3b56..2a55fb57b 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.6.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.6.2"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index 79fb1c1ac..35aef9288 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.6.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.6.2"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { diff --git a/libs/server-sdk-redis-source/CHANGELOG.md b/libs/server-sdk-redis-source/CHANGELOG.md index d85ad07d5..77908d6df 100644 --- a/libs/server-sdk-redis-source/CHANGELOG.md +++ b/libs/server-sdk-redis-source/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [2.1.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-server-redis-source-v2.1.9...launchdarkly-cpp-server-redis-source-v2.1.10) (2024-07-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-server bumped from 3.5.1 to 3.5.2 + ## [2.1.9](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-server-redis-source-v2.1.8...launchdarkly-cpp-server-redis-source-v2.1.9) (2024-06-11) diff --git a/libs/server-sdk-redis-source/CMakeLists.txt b/libs/server-sdk-redis-source/CMakeLists.txt index 2f2c8dd22..1f5cfc2fe 100644 --- a/libs/server-sdk-redis-source/CMakeLists.txt +++ b/libs/server-sdk-redis-source/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPServerRedisSource - VERSION 2.1.9 # {x-release-please-version} + VERSION 2.1.10 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Server SDK Redis Source" LANGUAGES CXX C ) diff --git a/libs/server-sdk-redis-source/package.json b/libs/server-sdk-redis-source/package.json index f15c513a8..b0a4c7eea 100644 --- a/libs/server-sdk-redis-source/package.json +++ b/libs/server-sdk-redis-source/package.json @@ -1,9 +1,9 @@ { "name": "launchdarkly-cpp-server-redis-source", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "2.1.9", + "version": "2.1.10", "private": true, "dependencies": { - "launchdarkly-cpp-server": "3.5.1" + "launchdarkly-cpp-server": "3.5.2" } } diff --git a/libs/server-sdk/CHANGELOG.md b/libs/server-sdk/CHANGELOG.md index 67696e97b..4a2bf8c76 100644 --- a/libs/server-sdk/CHANGELOG.md +++ b/libs/server-sdk/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [3.5.2](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-server-v3.5.1...launchdarkly-cpp-server-v3.5.2) (2024-07-15) + + +### Bug Fixes + +* more helpful error messages for streaming connection failures ([#419](https://github.com/launchdarkly/cpp-sdks/issues/419)) ([6bd21ba](https://github.com/launchdarkly/cpp-sdks/commit/6bd21ba1eafb5f19275935e1f62f7304d4dc69f5)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-sse-client bumped from 0.5.0 to 0.5.1 + ## [3.5.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-server-v3.5.0...launchdarkly-cpp-server-v3.5.1) (2024-06-11) diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt index dc81dc3e4..1ac339091 100644 --- a/libs/server-sdk/CMakeLists.txt +++ b/libs/server-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPServer - VERSION 3.5.1 # {x-release-please-version} + VERSION 3.5.2 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Server SDK" LANGUAGES CXX C ) diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index 177d639d4..b0b26069d 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -27,7 +27,8 @@ class IClient { /** Connects the client to LaunchDarkly's flag delivery endpoints. * - * StartAsync must be called to obtain fresh flag data. + * StartAsync must be called once for the SDK to start receiving flag data. + * It does not need to be called more than one time. * * The returned future will resolve to true or false based on the logic * outlined on @ref Initialized. @@ -41,6 +42,10 @@ class IClient { * * While the client is connecting asynchronously, it is safe to call * variation methods, which will return application-defined default values. + * + * The client will always continue to attempt to connect asynchronously + * after being started unless it encounters an unrecoverable error. The + * returned promise timing out does not affect this behavior. */ virtual std::future StartAsync() = 0; @@ -355,7 +360,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.5.1"; // {x-release-please-version} + "3.5.2"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/server-sdk/package.json b/libs/server-sdk/package.json index 1c36f9f9e..9b0eaf8d0 100644 --- a/libs/server-sdk/package.json +++ b/libs/server-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-server", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.5.1", + "version": "3.5.2", "private": true, "dependencies": { "launchdarkly-cpp-internal": "0.8.1", "launchdarkly-cpp-common": "1.7.0", - "launchdarkly-cpp-sse-client": "0.5.0" + "launchdarkly-cpp-sse-client": "0.5.1" } } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index 2e7de1a4f..68ddd3a0b 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -15,20 +15,6 @@ namespace launchdarkly::server_side::data_systems { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; -static char const* DataSourceErrorToString(sse::Error const error) { - switch (error) { - case sse::Error::NoContent: - return "server responded 204 (No Content), will not attempt to " - "reconnect"; - case sse::Error::InvalidRedirectLocation: - return "server responded with an invalid redirection"; - case sse::Error::UnrecoverableClientError: - return "unrecoverable client-side error"; - default: - return "unrecognized error"; - } -} - std::string const& StreamingDataSource::Identity() const { static std::string const identity = "streaming data source"; return identity; @@ -132,12 +118,12 @@ void StreamingDataSource::StartAsync( client_builder.errors([weak_self](auto error) { if (auto self = weak_self.lock()) { - auto error_string = DataSourceErrorToString(error); + std::string error_string = launchdarkly::sse::ErrorToString(error); LD_LOG(self->logger_, LogLevel::kError) << error_string; self->status_manager_.SetState( DataSourceStatus::DataSourceState::kOff, DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, - error_string); + std::move(error_string)); } }); diff --git a/libs/server-sdk/tests/client_test.cpp b/libs/server-sdk/tests/client_test.cpp index 646988048..77ec3a6bb 100644 --- a/libs/server-sdk/tests/client_test.cpp +++ b/libs/server-sdk/tests/client_test.cpp @@ -20,7 +20,7 @@ class ClientTest : public ::testing::Test { TEST_F(ClientTest, ClientConstructedWithMinimalConfigAndContextT) { char const* version = client_.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.5.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.5.2"); // {x-release-please-version} } TEST_F(ClientTest, BoolVariationDefaultPassesThrough) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index dc7f3c251..91d72c884 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -24,7 +24,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDServerSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.5.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.5.2"); // {x-release-please-version} LDServerSDK_Free(sdk); } diff --git a/libs/server-sent-events/CHANGELOG.md b/libs/server-sent-events/CHANGELOG.md index e38d2f0ec..0dbdc6f79 100644 --- a/libs/server-sent-events/CHANGELOG.md +++ b/libs/server-sent-events/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.5.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.5.0...launchdarkly-cpp-sse-client-v0.5.1) (2024-07-15) + + +### Bug Fixes + +* more helpful error messages for streaming connection failures ([#419](https://github.com/launchdarkly/cpp-sdks/issues/419)) ([6bd21ba](https://github.com/launchdarkly/cpp-sdks/commit/6bd21ba1eafb5f19275935e1f62f7304d4dc69f5)) + ## [0.5.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.4.0...launchdarkly-cpp-sse-client-v0.5.0) (2024-05-30) diff --git a/libs/server-sent-events/include/launchdarkly/sse/error.hpp b/libs/server-sent-events/include/launchdarkly/sse/error.hpp index df6ffe0ea..1345d983f 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/error.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/error.hpp @@ -1,11 +1,44 @@ #pragma once +#include +#include +#include +#include + +#include namespace launchdarkly::sse { +namespace errors { + +struct NoContent {}; +std::ostream& operator<<(std::ostream& out, NoContent const&); + +struct InvalidRedirectLocation { + std::string location; +}; +std::ostream& operator<<(std::ostream& out, InvalidRedirectLocation const&); + +struct NotRedirectable {}; +std::ostream& operator<<(std::ostream& out, NotRedirectable const&); -enum class Error { - NoContent = 1, - InvalidRedirectLocation = 2, - UnrecoverableClientError = 3, - ReadTimeout = 4, +struct ReadTimeout { + std::optional timeout; }; -} +std::ostream& operator<<(std::ostream& out, ReadTimeout const&); + +struct UnrecoverableClientError { + boost::beast::http::status status; +}; +std::ostream& operator<<(std::ostream& out, UnrecoverableClientError const&); + +} // namespace errors + +using Error = std::variant; + +std::ostream& operator<<(std::ostream& out, Error const& error); + +std::string ErrorToString(Error const& error); +} // namespace launchdarkly::sse diff --git a/libs/server-sent-events/package.json b/libs/server-sent-events/package.json index ef048d848..96fe4984f 100644 --- a/libs/server-sent-events/package.json +++ b/libs/server-sent-events/package.json @@ -2,6 +2,6 @@ "name": "launchdarkly-cpp-sse-client", "description": "This package.json exists for modeling dependencies for the release process.", "private": true, - "version": "0.5.0", + "version": "0.5.1", "dependencies": {} } diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index 8d5d754f9..be6d9d304 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(${LIBNAME} OBJECT client.cpp parser.cpp event.cpp + error.cpp backoff.cpp) target_link_libraries(${LIBNAME} diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index c2ad6dc3f..2ac6d1bc5 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -222,7 +222,7 @@ class FoxyClient : public Client, if (status_class == beast::http::status_class::successful) { if (response.result() == beast::http::status::no_content) { - errors_(Error::NoContent); + errors_(errors::NoContent{}); return; } if (!correct_content_type(response)) { @@ -247,14 +247,14 @@ class FoxyClient : public Client, auto new_url = redirect_url("base", location_header); if (!new_url) { - errors_(Error::InvalidRedirectLocation); + errors_(errors::InvalidRedirectLocation{location_header}); return; } req_.set(http::field::host, new_url->host()); req_.target(new_url->encoded_target()); } else { - errors_(Error::InvalidRedirectLocation); + errors_(errors::NotRedirectable{}); return; } } @@ -264,7 +264,7 @@ class FoxyClient : public Client, return async_backoff(backoff_reason(response.result())); } - errors_(Error::UnrecoverableClientError); + errors_(errors::UnrecoverableClientError{response.result()}); return; } @@ -284,7 +284,7 @@ class FoxyClient : public Client, return; } if (ec == boost::asio::error::operation_aborted) { - errors_(Error::ReadTimeout); + errors_(errors::ReadTimeout{read_timeout_}); return async_backoff( "aborting read of response body (timeout)"); } diff --git a/libs/server-sent-events/src/error.cpp b/libs/server-sent-events/src/error.cpp new file mode 100644 index 000000000..274641e77 --- /dev/null +++ b/libs/server-sent-events/src/error.cpp @@ -0,0 +1,63 @@ +#include + +#include + +namespace launchdarkly::sse { +namespace errors { + +std::ostream& operator<<(std::ostream& out, NoContent const&) { + out << "received HTTP error 204 (no content) - giving up " + "permanently"; + return out; +} + +std::ostream& operator<<(std::ostream& out, + InvalidRedirectLocation const& invalid) { + out << "received invalid redirect from server, cannot follow (" + << invalid.location << ") - giving up permanently"; + return out; +} + +std::ostream& operator<<(std::ostream& out, NotRedirectable const&) { + out << "received malformed redirect from server, cannot follow - giving up " + "permanently"; + return out; +} + +std::ostream& operator<<(std::ostream& out, ReadTimeout const& err) { + out << "timed out reading response body (exceeded " << err.timeout->count() + << "ms) - will retry"; + return out; +} + +std::ostream& operator<<(std::ostream& out, + UnrecoverableClientError const& err) { + std::string explanation; + if (err.status == boost::beast::http::status::unauthorized || + err.status == boost::beast::http::status::forbidden) { + explanation = " (invalid auth key)"; + } + out << "received HTTP error " << static_cast(err.status) << explanation + << " for streaming connection - giving up " + "permanently"; + return out; +} +} // namespace errors + +std::ostream& operator<<(std::ostream& out, Error const& error) { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + out << arg; + }, + error); + return out; +} + +std::string ErrorToString(Error const& error) { + std::stringstream ss; + ss << error; + return ss.str(); +} + +} // namespace launchdarkly::sse