Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions contract-tests/sse-contract-tests/src/event_outbox.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include "event_outbox.hpp"
#include "definitions.hpp"

#include <boost/beast/core/bind_handler.hpp>
#include <boost/url/parse.hpp>
#include <iostream>
#include <nlohmann/json.hpp>

#include <iostream>

// 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.
Expand Down Expand Up @@ -78,22 +80,8 @@ EventOutbox::RequestType EventOutbox::build_request(
json = EventMessage{"event", Event{std::move(arg)}};
}
} else if constexpr (std::is_same_v<T, launchdarkly::sse::Error>) {
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;
}
},
Expand Down
19 changes: 2 additions & 17 deletions libs/client-sdk/src/data_sources/streaming_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<config::shared::ClientSDK> const&
Expand Down Expand Up @@ -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));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
});

Expand Down
45 changes: 39 additions & 6 deletions libs/server-sent-events/include/launchdarkly/sse/error.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
#pragma once
#include <chrono>
#include <optional>
#include <ostream>
#include <variant>

#include <boost/beast/http/status.hpp>

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<std::chrono::milliseconds> 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<errors::NoContent,
errors::InvalidRedirectLocation,
errors::NotRedirectable,
errors::ReadTimeout,
errors::UnrecoverableClientError>;

std::ostream& operator<<(std::ostream& out, Error const& error);

std::string ErrorToString(Error const& error);
} // namespace launchdarkly::sse
1 change: 1 addition & 0 deletions libs/server-sent-events/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_library(${LIBNAME} OBJECT
client.cpp
parser.cpp
event.cpp
error.cpp
backoff.cpp)

target_link_libraries(${LIBNAME}
Expand Down
10 changes: 5 additions & 5 deletions libs/server-sent-events/src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}
}
Expand All @@ -264,7 +264,7 @@ class FoxyClient : public Client,
return async_backoff(backoff_reason(response.result()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does this get logged and what does it look like if it is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's what happens when there's a "recoverable" client error and a retry will occur. The log message looks like:

"Backing off in (N) seconds due to HTTP status 403 (Forbidden)"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thank you.

}

errors_(Error::UnrecoverableClientError);
errors_(errors::UnrecoverableClientError{response.result()});
return;
}

Expand All @@ -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)");
}
Expand Down
63 changes: 63 additions & 0 deletions libs/server-sent-events/src/error.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include <launchdarkly/sse/error.hpp>

#include <sstream>

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<int>(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<decltype(arg)>;
out << arg;
},
error);
return out;
}

std::string ErrorToString(Error const& error) {
std::stringstream ss;
ss << error;
return ss.str();
}

} // namespace launchdarkly::sse