14
14
#include < boost/beast/version.hpp>
15
15
16
16
#include < boost/url/parse.hpp>
17
-
17
+ # include < boost/url/url.hpp >
18
18
#include < chrono>
19
19
#include < iostream>
20
20
#include < memory>
@@ -58,6 +58,7 @@ class FoxyClient : public Client,
58
58
std::optional<std::chrono::milliseconds> write_timeout,
59
59
Builder::EventReceiver receiver,
60
60
Builder::LogCallback logger,
61
+ Builder::ErrorCallback errors,
61
62
std::optional<net::ssl::context> maybe_ssl)
62
63
: ssl_context_(std::move(maybe_ssl)),
63
64
host_ (std::move(host)),
@@ -74,12 +75,13 @@ class FoxyClient : public Client,
74
75
last_event_id_(std::nullopt),
75
76
backoff_timer_(session_.get_executor()),
76
77
event_receiver_(std::move(receiver)),
77
- logger_(std::move(logger)) {
78
+ logger_(std::move(logger)),
79
+ errors_(std::move(errors)) {
78
80
create_parser ();
79
81
}
80
82
81
- /* * The body parser is recreated each time a connection is made because its
82
- * internal state cannot be explicitly reset.
83
+ /* * The body parser is recreated each time a connection is made because
84
+ * its internal state cannot be explicitly reset.
83
85
*
84
86
* Since SSE body will never end unless
85
87
* an error occurs, the body size limit must be removed.
@@ -100,6 +102,12 @@ class FoxyClient : public Client,
100
102
void do_backoff (std::string const & reason) {
101
103
backoff_.fail ();
102
104
105
+ if (auto id = body_parser_->get ().body ().last_event_id ()) {
106
+ if (!id->empty ()) {
107
+ last_event_id_ = id;
108
+ }
109
+ }
110
+
103
111
std::stringstream msg;
104
112
msg << " backing off in ("
105
113
<< std::chrono::duration_cast<std::chrono::seconds>(
@@ -109,7 +117,6 @@ class FoxyClient : public Client,
109
117
110
118
logger_ (msg.str ());
111
119
112
- last_event_id_ = body_parser_->get ().body ().last_event_id ();
113
120
create_parser ();
114
121
backoff_timer_.expires_from_now (backoff_.delay ());
115
122
backoff_timer_.async_wait (beast::bind_front_handler (
@@ -138,7 +145,7 @@ class FoxyClient : public Client,
138
145
return do_backoff (ec.what ());
139
146
}
140
147
141
- if (last_event_id_ && !last_event_id_-> empty () ) {
148
+ if (last_event_id_) {
142
149
req_.set (" last-event-id" , *last_event_id_);
143
150
} else {
144
151
req_.erase (" last-event-id" );
@@ -186,6 +193,10 @@ class FoxyClient : public Client,
186
193
auto status_class = beast::http::to_status_class (response.result ());
187
194
188
195
if (status_class == beast::http::status_class::successful) {
196
+ if (response.result () == beast::http::status::no_content) {
197
+ errors_ (Error::NoContent);
198
+ return ;
199
+ }
189
200
if (!correct_content_type (response)) {
190
201
return do_backoff (" invalid Content-Type" );
191
202
}
@@ -197,13 +208,30 @@ class FoxyClient : public Client,
197
208
shared_from_this ()));
198
209
}
199
210
211
+ if (status_class == beast::http::status_class::redirection) {
212
+ if (can_redirect (response)) {
213
+ auto new_url =
214
+ redirect_url (" base" , response.find (" location" )->value ());
215
+
216
+ if (!new_url) {
217
+ errors_ (Error::InvalidRedirectLocation);
218
+ return ;
219
+ }
220
+
221
+ req_.set (http::field::host, new_url->host ());
222
+ req_.target (new_url->encoded_target ());
223
+ } else {
224
+ errors_ (Error::InvalidRedirectLocation);
225
+ return ;
226
+ }
227
+ }
228
+
200
229
if (status_class == beast::http::status_class::client_error) {
201
230
if (recoverable_client_error (response.result ())) {
202
231
return do_backoff (backoff_reason (response.result ()));
203
232
}
204
233
205
- // TODO: error callback
206
-
234
+ errors_ (Error::UnrecoverableClientError);
207
235
return ;
208
236
}
209
237
@@ -244,11 +272,6 @@ class FoxyClient : public Client,
244
272
}
245
273
}
246
274
247
- void fail (boost::system::error_code ec, std::string const & what) {
248
- logger_ (" sse-client: " + what + " : " + ec.message ());
249
- async_shutdown (nullptr );
250
- }
251
-
252
275
static bool recoverable_client_error (beast::http::status status) {
253
276
return (status == beast::http::status::bad_request ||
254
277
status == beast::http::status::request_timeout ||
@@ -264,21 +287,52 @@ class FoxyClient : public Client,
264
287
return false ;
265
288
}
266
289
290
+ static bool can_redirect (FoxyClient::response const & response) {
291
+ return (response.result () == beast::http::status::moved_permanently ||
292
+ response.result () == beast::http::status::temporary_redirect) &&
293
+ response.find (" location" ) != response.end ();
294
+ }
295
+
296
+ static std::optional<boost::urls::url> redirect_url (
297
+ std::string orig_base,
298
+ std::string orig_location) {
299
+ auto location = boost::urls::parse_uri (orig_location);
300
+ if (!location) {
301
+ return std::nullopt;
302
+ }
303
+ if (location->has_scheme ()) {
304
+ return location.value ();
305
+ }
306
+
307
+ boost::urls::url base (orig_base);
308
+ auto result = base.resolve (*location);
309
+ if (!result) {
310
+ return std::nullopt;
311
+ }
312
+
313
+ return base;
314
+ }
315
+
267
316
private:
268
317
std::optional<net::ssl::context> ssl_context_;
269
318
std::string host_;
270
319
std::string port_;
320
+
271
321
std::optional<std::chrono::milliseconds> connect_timeout_;
272
322
std::optional<std::chrono::milliseconds> read_timeout_;
273
323
std::optional<std::chrono::milliseconds> write_timeout_;
324
+
274
325
http::request<http::string_body> req_;
326
+
275
327
Builder::EventReceiver event_receiver_;
276
- std::optional<http::response_parser<body> > body_parser_;
328
+ Builder::LogCallback logger_;
329
+ Builder::ErrorCallback errors_;
330
+
331
+ std::optional<http::response_parser<body>> body_parser_;
277
332
launchdarkly::foxy::client_session session_;
278
333
std::optional<std::string> last_event_id_;
279
334
Backoff backoff_;
280
335
boost::asio::steady_timer backoff_timer_;
281
- Builder::LogCallback logger_;
282
336
};
283
337
284
338
Builder::Builder (net::any_io_executor ctx, std::string url)
@@ -287,9 +341,9 @@ Builder::Builder(net::any_io_executor ctx, std::string url)
287
341
read_timeout_{std::nullopt},
288
342
write_timeout_{std::nullopt},
289
343
connect_timeout_{std::nullopt},
290
- logging_cb_ ([](auto msg) {}) {
291
- receiver_ = [](launchdarkly::sse::Event const &) {};
292
-
344
+ logging_cb_ ([](auto msg) {}),
345
+ receiver_ ( [](launchdarkly::sse::Event const &) {}),
346
+ error_cb_ ([]( auto err) {}) {
293
347
request_.version (11 );
294
348
request_.set (http::field::user_agent, kDefaultUserAgent );
295
349
request_.method (http::verb::get);
@@ -332,11 +386,16 @@ Builder& Builder::receiver(EventReceiver receiver) {
332
386
return *this ;
333
387
}
334
388
335
- Builder& Builder::logger (std::function< void (std::string)> callback) {
389
+ Builder& Builder::logger (LogCallback callback) {
336
390
logging_cb_ = std::move (callback);
337
391
return *this ;
338
392
}
339
393
394
+ Builder& Builder::errors (ErrorCallback callback) {
395
+ error_cb_ = std::move (callback);
396
+ return *this ;
397
+ }
398
+
340
399
std::shared_ptr<Client> Builder::build () {
341
400
auto uri_components = boost::urls::parse_uri (url_);
342
401
if (!uri_components) {
@@ -376,7 +435,8 @@ std::shared_ptr<Client> Builder::build() {
376
435
377
436
return std::make_shared<FoxyClient>(
378
437
net::make_strand (executor_), request, host, service, connect_timeout_,
379
- read_timeout_, write_timeout_, receiver_, logging_cb_, std::move (ssl));
438
+ read_timeout_, write_timeout_, receiver_, logging_cb_, error_cb_,
439
+ std::move (ssl));
380
440
}
381
441
382
442
} // namespace launchdarkly::sse
0 commit comments