From a80ab1179bccb6ec204f9d3b47e4c68739af1c00 Mon Sep 17 00:00:00 2001 From: Jonathan Reams Date: Tue, 8 Aug 2023 15:52:39 -0400 Subject: [PATCH] Handle websocket errors entirely within sync client (#6859) --- CHANGELOG.md | 2 +- src/realm.h | 4 +- .../object-store/c_api/socket_provider.cpp | 21 +- src/realm/object-store/c_api/sync.cpp | 8 +- .../sync/impl/emscripten/socket_provider.cpp | 4 +- src/realm/object-store/sync/sync_session.cpp | 52 ++--- src/realm/sync/client.cpp | 2 +- src/realm/sync/client_base.hpp | 4 +- src/realm/sync/network/default_socket.cpp | 49 ++-- src/realm/sync/network/websocket.cpp | 213 +++++++++--------- src/realm/sync/network/websocket.hpp | 45 +--- src/realm/sync/network/websocket_error.hpp | 61 +++++ src/realm/sync/noinst/client_impl_base.cpp | 130 ++++++----- src/realm/sync/noinst/client_impl_base.hpp | 8 +- src/realm/sync/noinst/protocol_codec.hpp | 8 +- src/realm/sync/protocol.hpp | 25 +- src/realm/sync/socket_provider.hpp | 8 +- test/object-store/c_api/c_api.cpp | 16 +- test/object-store/sync/client_reset.cpp | 6 +- test/object-store/sync/session/session.cpp | 16 +- .../sync/session/wait_for_completion.cpp | 8 +- test/sync_fixtures.hpp | 6 +- test/test_sync.cpp | 13 +- test/test_util_websocket.cpp | 7 +- 24 files changed, 341 insertions(+), 375 deletions(-) create mode 100644 src/realm/sync/network/websocket_error.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5c87b729c..dd5fa2f2f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ * Fix failed assertion for unknown app server errors ([#6758](https://github.com/realm/realm-core/issues/6758), since v12.9.0). ### Breaking changes -* None. +* The `WebSocketObserver` interface in the sync `SocketProvider` API now takes a `WebSocketError` enum/`std::string_view` for the `websocket_closed_handler()` instead of a `Status`. Implementers of platform networking should make sure all their error handling is expressed in terms of the WebSocketError enum. ([PR #6859](https://github.com/realm/realm-core/pull/6859)) ### Compatibility * Fileformat: Generates files with format v23. Reads and automatically upgrade from fileformat v5. diff --git a/src/realm.h b/src/realm.h index 9705486d4f9..3966683bafa 100644 --- a/src/realm.h +++ b/src/realm.h @@ -4106,8 +4106,8 @@ RLM_API realm_sync_socket_t* realm_sync_socket_new( realm_sync_socket_websocket_async_write_func_t websocket_write_func, realm_sync_socket_websocket_free_func_t websocket_free_func); -RLM_API void realm_sync_socket_callback_complete(realm_sync_socket_callback_t* realm_callback, - realm_web_socket_errno_e status, const char* reason); +RLM_API void realm_sync_socket_callback_complete(realm_sync_socket_callback_t* realm_callback, realm_errno_e status, + const char* reason); RLM_API void realm_sync_socket_websocket_connected(realm_websocket_observer_t* realm_websocket_observer, const char* protocol); diff --git a/src/realm/object-store/c_api/socket_provider.cpp b/src/realm/object-store/c_api/socket_provider.cpp index a43d4fa1a87..47c005005b5 100644 --- a/src/realm/object-store/c_api/socket_provider.cpp +++ b/src/realm/object-store/c_api/socket_provider.cpp @@ -122,9 +122,9 @@ struct CAPIWebSocketObserver : sync::WebSocketObserver { return m_observer->websocket_binary_message_received(data); } - bool websocket_closed_handler(bool was_clean, Status status) final + bool websocket_closed_handler(bool was_clean, sync::websocket::WebSocketError code, std::string_view msg) final { - return m_observer->websocket_closed_handler(was_clean, status); + return m_observer->websocket_closed_handler(was_clean, code, msg); } private: @@ -218,13 +218,11 @@ RLM_API realm_sync_socket_t* realm_sync_socket_new( }); } -RLM_API void realm_sync_socket_callback_complete(realm_sync_socket_callback* realm_callback, - realm_web_socket_errno_e code, const char* reason) +RLM_API void realm_sync_socket_callback_complete(realm_sync_socket_callback* realm_callback, realm_errno_e code, + const char* reason) { - auto status = sync::websocket::WebSocketError(code); - auto complete_status = code == realm_web_socket_errno_e::RLM_ERR_WEBSOCKET_OK - ? Status::OK() - : Status{sync::websocket::make_error_code(status), reason}; + auto complete_status = + code == realm_errno_e::RLM_ERR_NONE ? Status::OK() : Status{static_cast(code), reason}; (*(realm_callback->get()))(complete_status); realm_release(realm_callback); } @@ -249,11 +247,8 @@ RLM_API void realm_sync_socket_websocket_message(realm_websocket_observer_t* rea RLM_API void realm_sync_socket_websocket_closed(realm_websocket_observer_t* realm_websocket_observer, bool was_clean, realm_web_socket_errno_e code, const char* reason) { - auto status = sync::websocket::WebSocketError(code); - auto closed_status = code == realm_web_socket_errno_e::RLM_ERR_WEBSOCKET_OK - ? Status::OK() - : Status{sync::websocket::make_error_code(status), reason}; - realm_websocket_observer->get()->websocket_closed_handler(was_clean, closed_status); + realm_websocket_observer->get()->websocket_closed_handler( + was_clean, static_cast(code), reason); } RLM_API void realm_sync_client_config_set_sync_socket(realm_sync_client_config_t* config, diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 945d5fbb1e8..0ab903dd1c3 100644 --- a/src/realm/object-store/c_api/sync.cpp +++ b/src/realm/object-store/c_api/sync.cpp @@ -144,9 +144,6 @@ realm_sync_error_code_t to_capi(const Status& status, std::string& message) else if (category == std::system_category() || category == realm::util::error::basic_system_error_category()) { ret.category = RLM_SYNC_ERROR_CATEGORY_SYSTEM; } - else if (category == realm::sync::websocket::websocket_error_category()) { - ret.category = RLM_SYNC_ERROR_CATEGORY_WEBSOCKET; - } else { ret.category = RLM_SYNC_ERROR_CATEGORY_UNKNOWN; } @@ -178,9 +175,6 @@ void sync_error_to_error_code(const realm_sync_error_code_t& sync_error_code, st else if (category == RLM_SYNC_ERROR_CATEGORY_SYSTEM) { error_code_out->assign(sync_error_code.value, std::system_category()); } - else if (category == RLM_SYNC_ERROR_CATEGORY_WEBSOCKET) { - error_code_out->assign(sync_error_code.value, realm::sync::websocket::websocket_error_category()); - } else if (category == RLM_SYNC_ERROR_CATEGORY_UNKNOWN) { error_code_out->assign(sync_error_code.value, realm::util::error::basic_system_error_category()); } @@ -902,7 +896,7 @@ RLM_API void realm_sync_session_handle_error_for_testing(const realm_sync_sessio std::error_code err; sync_error_to_error_code(sync_error, &err); SyncSession::OnlyForTesting::handle_error(*session->get(), - sync::SessionErrorInfo{Status{err, error_message}, !is_fatal}); + sync::SessionErrorInfo{Status{err, error_message}, IsFatal{is_fatal}}); } } // namespace realm::c_api diff --git a/src/realm/object-store/sync/impl/emscripten/socket_provider.cpp b/src/realm/object-store/sync/impl/emscripten/socket_provider.cpp index ea01d61c3c6..6df6e77b61d 100644 --- a/src/realm/object-store/sync/impl/emscripten/socket_provider.cpp +++ b/src/realm/object-store/sync/impl/emscripten/socket_provider.cpp @@ -187,8 +187,8 @@ struct EmscriptenWebSocket final : public WebSocketInterface { { auto observer = reinterpret_cast(user_data); REALM_ASSERT(event->code >= 1000 && event->code < 5000); - auto status = event->code == 1000 ? Status::OK() : Status(ErrorCodes::Error(event->code), event->reason); - observer->websocket_closed_handler(event->wasClean, std::move(status)); + observer->websocket_closed_handler(event->wasClean, static_cast(event->code), + event->reason); return EM_TRUE; } diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index 52e99809074..2b79f34501a 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -579,11 +579,10 @@ void SyncSession::handle_fresh_realm_downloaded(DBRef db, Status status, } lock.unlock(); - const bool try_again = false; sync::SessionErrorInfo synthetic( Status{ErrorCodes::AutoClientResetFailed, util::format("A fatal error occurred during client reset: '%1'", status.reason())}, - try_again); + sync::IsFatal{true}); handle_error(synthetic); return; } @@ -633,7 +632,7 @@ void SyncSession::OnlyForTesting::handle_error(SyncSession& session, sync::Sessi void SyncSession::handle_error(sync::SessionErrorInfo error) { enum class NextStateAfterError { none, inactive, error }; - auto next_state = error.is_fatal() ? NextStateAfterError::error : NextStateAfterError::none; + auto next_state = error.is_fatal ? NextStateAfterError::error : NextStateAfterError::none; auto error_code = error.status.get_std_error_code(); util::Optional delete_file; bool log_out_user = false; @@ -708,46 +707,27 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) save_sync_config_after_migration_or_rollback(); download_fresh_realm(error.server_requests_action); return; + case sync::ProtocolErrorInfo::Action::RefreshUser: + if (auto u = user()) { + u->refresh_custom_data(false, handle_refresh(shared_from_this(), false)); + return; + } + break; + case sync::ProtocolErrorInfo::Action::RefreshLocation: + if (auto u = user()) { + u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); + return; + } + break; } } - else if (error_code.category() == sync::websocket::websocket_error_category()) { - using WebSocketError = sync::websocket::WebSocketError; - auto websocket_error = static_cast(error_code.value()); - - // The server replies with '401: unauthorized' if the access token is invalid, expired, revoked, or the user - // is disabled. In this scenario we attempt an automatic token refresh and if that succeeds continue as - // normal. If the refresh request also fails with 401 then we need to stop retrying and pass along the error; - // see handle_refresh(). - bool redirect_occurred = websocket_error == WebSocketError::websocket_moved_permanently; - if (redirect_occurred || websocket_error == WebSocketError::websocket_unauthorized || - websocket_error == WebSocketError::websocket_abnormal_closure) { - if (auto u = user()) { - // If a redirection occurred, the location metadata will be updated before refreshing the access - // token. - u->refresh_custom_data(redirect_occurred, handle_refresh(shared_from_this(), redirect_occurred)); - return; - } - } - - // If the websocket was closed cleanly or if the socket disappeared, don't notify the user as an error - // since the sync client will retry. - if (websocket_error == WebSocketError::websocket_read_error || - websocket_error == WebSocketError::websocket_write_error) { - return; - } - - // Surface a simplified websocket error to the user. - auto simplified_error = sync::websocket::get_simplified_websocket_error(websocket_error); - std::error_code new_error_code(simplified_error, sync::websocket::websocket_error_category()); - error = sync::SessionErrorInfo(Status{new_error_code, error.message}, error.try_again); - } else { // Unrecognized error code. unrecognized_by_client = true; } util::CheckedUniqueLock lock(m_state_mutex); - SyncError sync_error{error.status, error.is_fatal(), error.log_url, std::move(error.compensating_writes)}; + SyncError sync_error{error.status, error.is_fatal, error.log_url, std::move(error.compensating_writes)}; // `action` is used over `shouldClientReset` and `isRecoveryModeDisabled`. sync_error.server_requests_action = error.server_requests_action; sync_error.is_unrecognized_by_client = unrecognized_by_client; @@ -755,7 +735,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) if (delete_file) update_error_and_mark_file_for_deletion(sync_error, *delete_file); - if (m_state == State::Dying && error.is_fatal()) { + if (m_state == State::Dying && error.is_fatal) { become_inactive(std::move(lock), error.status); return; } diff --git a/src/realm/sync/client.cpp b/src/realm/sync/client.cpp index 8a4287dcd74..cd3f23884f7 100644 --- a/src/realm/sync/client.cpp +++ b/src/realm/sync/client.cpp @@ -1000,7 +1000,7 @@ SyncClientHookAction SessionImpl::call_debug_hook(const SyncClientHookData& data auto action = m_wrapper.m_debug_hook(data); switch (action) { case realm::SyncClientHookAction::SuspendWithRetryableError: { - SessionErrorInfo err_info(Status{ErrorCodes::RuntimeError, "hook requested error"}, true); + SessionErrorInfo err_info(Status{ErrorCodes::RuntimeError, "hook requested error"}, IsFatal{false}); err_info.server_requests_action = ProtocolErrorInfo::Action::Transient; auto err_processing_err = receive_error_message(err_info); diff --git a/src/realm/sync/client_base.hpp b/src/realm/sync/client_base.hpp index e3cb0f59096..e72d1d36a8c 100644 --- a/src/realm/sync/client_base.hpp +++ b/src/realm/sync/client_base.hpp @@ -251,8 +251,8 @@ struct SessionErrorInfo : public ProtocolErrorInfo { { } - SessionErrorInfo(Status status, bool try_again) - : ProtocolErrorInfo(status.get_std_error_code().value(), status.reason(), try_again) + SessionErrorInfo(Status status, IsFatal is_fatal) + : ProtocolErrorInfo(status.get_std_error_code().value(), status.reason(), is_fatal) , status(std::move(status)) { } diff --git a/src/realm/sync/network/default_socket.cpp b/src/realm/sync/network/default_socket.cpp index 9032bc3cd11..e7617afb671 100644 --- a/src/realm/sync/network/default_socket.cpp +++ b/src/realm/sync/network/default_socket.cpp @@ -78,15 +78,13 @@ class DefaultWebSocketImpl final : public DefaultWebSocket, public Config { { m_logger.error("Reading failed: %1", ec.message()); // Throws constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, Status{make_error_code(WebSocketError::websocket_read_error), ec.message()}); + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_read_error, ec.message()); } void websocket_write_error_handler(std::error_code ec) override { m_logger.error("Writing failed: %1", ec.message()); // Throws constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, Status{make_error_code(WebSocketError::websocket_write_error), ec.message()}); + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_write_error, ec.message()); } void websocket_handshake_error_handler(std::error_code ec, const HTTPHeaders*, const std::string_view* body) override @@ -144,30 +142,25 @@ class DefaultWebSocketImpl final : public DefaultWebSocket, public Config { } } - websocket_error_and_close_handler(was_clean, Status{make_error_code(error), ec.message()}); + websocket_error_and_close_handler(was_clean, error, ec.message()); } void websocket_protocol_error_handler(std::error_code ec) override { constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, Status{make_error_code(WebSocketError::websocket_protocol_error), ec.message()}); + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_protocol_error, ec.message()); } - bool websocket_close_message_received(std::error_code ec, StringData message) override + bool websocket_close_message_received(WebSocketError code, std::string_view message) override { constexpr bool was_clean = true; - // Normal closure. - if (ec.value() == 1000) { - return websocket_error_and_close_handler(was_clean, Status::OK()); - } - return websocket_error_and_close_handler(was_clean, Status{ec, message}); + return websocket_error_and_close_handler(was_clean, code, message); } - bool websocket_error_and_close_handler(bool was_clean, Status status) + bool websocket_error_and_close_handler(bool was_clean, WebSocketError code, std::string_view reason) { if (!was_clean) { m_observer->websocket_error_handler(); } - return m_observer->websocket_closed_handler(was_clean, status); + return m_observer->websocket_closed_handler(was_clean, code, reason); } bool websocket_binary_message_received(const char* ptr, std::size_t size) override { @@ -275,8 +268,8 @@ void DefaultWebSocketImpl::handle_resolve(std::error_code ec, network::Endpoint: if (ec) { m_logger.error("Failed to resolve '%1:%2': %3", m_endpoint.address, m_endpoint.port, ec.message()); // Throws constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, Status{make_error_code(WebSocketError::websocket_resolve_failed), ec.message()}); // Throws + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_resolve_failed, + ec.message()); // Throws return; } @@ -316,8 +309,8 @@ void DefaultWebSocketImpl::handle_tcp_connect(std::error_code ec, network::Endpo // All endpoints failed m_logger.error("Failed to connect to '%1:%2': All endpoints failed", m_endpoint.address, m_endpoint.port); constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, Status{make_error_code(WebSocketError::websocket_connection_failed), ec.message()}); // Throws + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_connection_failed, + ec.message()); // Throws return; } @@ -357,18 +350,16 @@ void DefaultWebSocketImpl::initiate_http_tunnel() if (ec && ec != util::error::operation_aborted) { m_logger.error("Failed to establish HTTP tunnel: %1", ec.message()); constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, - Status{make_error_code(WebSocketError::websocket_connection_failed), ec.message()}); // Throws + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_connection_failed, + ec.message()); // Throws return; } if (response.status != HTTPStatus::Ok) { m_logger.error("Proxy server returned response '%1 %2'", response.status, response.reason); // Throws constexpr bool was_clean = false; - websocket_error_and_close_handler( - was_clean, - Status{make_error_code(WebSocketError::websocket_connection_failed), response.reason}); // Throws + websocket_error_and_close_handler(was_clean, WebSocketError::websocket_connection_failed, + response.reason); // Throws return; } @@ -431,15 +422,15 @@ void DefaultWebSocketImpl::handle_ssl_handshake(std::error_code ec) if (ec) { REALM_ASSERT(ec != util::error::operation_aborted); constexpr bool was_clean = false; - std::error_code ec2; + WebSocketError parsed_error_code; if (ec == network::ssl::Errors::certificate_rejected) { - ec2 = make_error_code(WebSocketError::websocket_tls_handshake_failed); + parsed_error_code = WebSocketError::websocket_tls_handshake_failed; } else { - ec2 = make_error_code(WebSocketError::websocket_connection_failed); + parsed_error_code = WebSocketError::websocket_connection_failed; } - websocket_error_and_close_handler(was_clean, Status{ec2, ec.message()}); // Throws + websocket_error_and_close_handler(was_clean, parsed_error_code, ec.message()); // Throws return; } diff --git a/src/realm/sync/network/websocket.cpp b/src/realm/sync/network/websocket.cpp index e91d982598d..cc9c12cc5e6 100644 --- a/src/realm/sync/network/websocket.cpp +++ b/src/realm/sync/network/websocket.cpp @@ -944,10 +944,10 @@ class WebSocket { return true; } - std::pair parse_close_message(const char* data, size_t size) + std::pair parse_close_message(const char* data, size_t size) { uint16_t error_code; - StringData error_message; + std::string_view error_message; if (size < 2) { // Error code 1005 is defined as // 1005 is a reserved value and MUST NOT be set as a status code in a @@ -962,11 +962,36 @@ class WebSocket { // network byte order. See https://tools.ietf.org/html/rfc6455#section-5.5.1 for more // details. error_code = ntohs((uint8_t(data[1]) << 8) | uint8_t(data[0])); - error_message = StringData(data + 2, size - 2); + error_message = std::string_view(data + 2, size - 2); } - std::error_code error_code_with_category{error_code, websocket::websocket_error_category()}; - return std::make_pair(error_code_with_category, error_message); + switch (static_cast(error_code)) { + case WebSocketError::websocket_ok: + case WebSocketError::websocket_going_away: + case WebSocketError::websocket_protocol_error: + case WebSocketError::websocket_unsupported_data: + case WebSocketError::websocket_reserved: + case WebSocketError::websocket_no_status_received: + case WebSocketError::websocket_abnormal_closure: + case WebSocketError::websocket_invalid_payload_data: + case WebSocketError::websocket_policy_violation: + case WebSocketError::websocket_message_too_big: + case WebSocketError::websocket_invalid_extension: + case WebSocketError::websocket_internal_server_error: + case WebSocketError::websocket_tls_handshake_failed: + + case WebSocketError::websocket_unauthorized: + case WebSocketError::websocket_forbidden: + case WebSocketError::websocket_moved_permanently: + case WebSocketError::websocket_client_too_old: + case WebSocketError::websocket_client_too_new: + case WebSocketError::websocket_protocol_mismatch: + break; + default: + error_code = 1008; + } + + return std::make_pair(static_cast(error_code), error_message); } // frame_reader_loop() uses the frame_reader to read and process the incoming @@ -1116,85 +1141,82 @@ class HttpErrorCategory : public std::error_category { } }; -std::string error_string(WebSocketError code) +} // unnamed namespace + +namespace realm::sync::websocket { + +std::ostream& operator<<(std::ostream& os, WebSocketError code) { /// WebSocket error codes - switch (code) { - case WebSocketError::websocket_ok: - return "WebSocket: OK"; - case WebSocketError::websocket_going_away: - return "WebSocket: Going Away"; - case WebSocketError::websocket_protocol_error: - return "WebSocket: Protocol Error"; - case WebSocketError::websocket_unsupported_data: - return "WebSocket: Unsupported Data"; - case WebSocketError::websocket_reserved: - return "WebSocket: Reserved"; - case WebSocketError::websocket_no_status_received: - return "WebSocket: No Status Received"; - case WebSocketError::websocket_abnormal_closure: - return "WebSocket: Abnormal Closure"; - case WebSocketError::websocket_invalid_payload_data: - return "WebSocket: Invalid Payload Data"; - case WebSocketError::websocket_policy_violation: - return "WebSocket: Policy Violation"; - case WebSocketError::websocket_message_too_big: - return "WebSocket: Message Too Big"; - case WebSocketError::websocket_invalid_extension: - return "WebSocket: Invalid Extension"; - case WebSocketError::websocket_internal_server_error: - return "WebSocket: Internal Server Error"; - case WebSocketError::websocket_tls_handshake_failed: - return "WebSocket: TLS Handshake Failed"; - - /// WebSocket Errors - reported by server - case WebSocketError::websocket_unauthorized: - return "WebSocket: Unauthorized"; - case WebSocketError::websocket_forbidden: - return "WebSocket: Forbidden"; - case WebSocketError::websocket_moved_permanently: - return "WebSocket: Moved Permanently"; - case WebSocketError::websocket_client_too_old: - return "WebSocket: Client Too Old"; - case WebSocketError::websocket_client_too_new: - return "WebSocket: Client Too New"; - case WebSocketError::websocket_protocol_mismatch: - return "WebSocket: Protocol Mismatch"; - - case WebSocketError::websocket_resolve_failed: - return "WebSocket: Resolve Failed"; - case WebSocketError::websocket_connection_failed: - return "WebSocket: Connection Failed"; - case WebSocketError::websocket_read_error: - return "WebSocket: Read Error"; - case WebSocketError::websocket_write_error: - return "WebSocket: Write Error"; - case WebSocketError::websocket_retry_error: - return "WebSocket: Retry Error"; - case WebSocketError::websocket_fatal_error: - return "WebSocket: Fatal Error"; - } - return ""; -} + auto str = [&]() -> const char* { + switch (code) { + case WebSocketError::websocket_ok: + return "WebSocket: OK"; + case WebSocketError::websocket_going_away: + return "WebSocket: Going Away"; + case WebSocketError::websocket_protocol_error: + return "WebSocket: Protocol Error"; + case WebSocketError::websocket_unsupported_data: + return "WebSocket: Unsupported Data"; + case WebSocketError::websocket_reserved: + return "WebSocket: Reserved"; + case WebSocketError::websocket_no_status_received: + return "WebSocket: No Status Received"; + case WebSocketError::websocket_abnormal_closure: + return "WebSocket: Abnormal Closure"; + case WebSocketError::websocket_invalid_payload_data: + return "WebSocket: Invalid Payload Data"; + case WebSocketError::websocket_policy_violation: + return "WebSocket: Policy Violation"; + case WebSocketError::websocket_message_too_big: + return "WebSocket: Message Too Big"; + case WebSocketError::websocket_invalid_extension: + return "WebSocket: Invalid Extension"; + case WebSocketError::websocket_internal_server_error: + return "WebSocket: Internal Server Error"; + case WebSocketError::websocket_tls_handshake_failed: + return "WebSocket: TLS Handshake Failed"; + + /// WebSocket Errors - reported by server + case WebSocketError::websocket_unauthorized: + return "WebSocket: Unauthorized"; + case WebSocketError::websocket_forbidden: + return "WebSocket: Forbidden"; + case WebSocketError::websocket_moved_permanently: + return "WebSocket: Moved Permanently"; + case WebSocketError::websocket_client_too_old: + return "WebSocket: Client Too Old"; + case WebSocketError::websocket_client_too_new: + return "WebSocket: Client Too New"; + case WebSocketError::websocket_protocol_mismatch: + return "WebSocket: Protocol Mismatch"; + + case WebSocketError::websocket_resolve_failed: + return "WebSocket: Resolve Failed"; + case WebSocketError::websocket_connection_failed: + return "WebSocket: Connection Failed"; + case WebSocketError::websocket_read_error: + return "WebSocket: Read Error"; + case WebSocketError::websocket_write_error: + return "WebSocket: Write Error"; + case WebSocketError::websocket_retry_error: + return "WebSocket: Retry Error"; + case WebSocketError::websocket_fatal_error: + return "WebSocket: Fatal Error"; + } + return nullptr; + }(); -class WebSocketErrorCategory : public std::error_category { - const char* name() const noexcept final - { - return "realm::sync::websocket::WebSocketError"; + if (str == nullptr) { + os << "WebSocket: Unknown Error (" << static_cast>(code) << ")"; } - std::string message(int error_code) const final - { - // Converts an error_code to one of the pre-defined status codes in - // https://tools.ietf.org/html/rfc6455#section-7.4.1 - auto msg = error_string(static_cast(error_code)); - if (msg.empty()) - msg = "Unknown error"; - return msg; + else { + os << str; } -}; - -} // unnamed namespace + return os; +} +} // namespace realm::sync::websocket bool websocket::Config::websocket_text_message_received(const char*, size_t) { @@ -1206,7 +1228,7 @@ bool websocket::Config::websocket_binary_message_received(const char*, size_t) return true; } -bool websocket::Config::websocket_close_message_received(std::error_code, StringData) +bool websocket::Config::websocket_close_message_received(WebSocketError, std::string_view) { return true; } @@ -1314,17 +1336,6 @@ util::Optional websocket::make_http_response(const HTTPRequest& re return do_make_http_response(request, sec_websocket_protocol, ec); } -const std::error_category& websocket::websocket_error_category() noexcept -{ - static const WebSocketErrorCategory category = {}; - return category; -} - -std::error_code websocket::make_error_code(WebSocketError error) noexcept -{ - return std::error_code{int(error), websocket_error_category()}; -} - const std::error_category& websocket::http_error_category() noexcept { static const HttpErrorCategory category = {}; @@ -1335,25 +1346,3 @@ std::error_code websocket::make_error_code(HttpError error_code) noexcept { return std::error_code{int(error_code), http_error_category()}; } - -ErrorCodes::Error websocket::get_simplified_websocket_error(WebSocketError error) -{ - if (error == sync::websocket::WebSocketError::websocket_resolve_failed) { - return ErrorCodes::WebSocketResolveFailedError; - } - else if (error == sync::websocket::WebSocketError::websocket_connection_failed || - error == sync::websocket::WebSocketError::websocket_unauthorized || - error == sync::websocket::WebSocketError::websocket_forbidden || - error == sync::websocket::WebSocketError::websocket_moved_permanently || - error == sync::websocket::WebSocketError::websocket_client_too_old || - error == sync::websocket::WebSocketError::websocket_client_too_new || - error == sync::websocket::WebSocketError::websocket_protocol_mismatch || - error == sync::websocket::WebSocketError::websocket_read_error || - error == sync::websocket::WebSocketError::websocket_write_error || - error == sync::websocket::WebSocketError::websocket_retry_error || - error == sync::websocket::WebSocketError::websocket_fatal_error) { - return ErrorCodes::WebSocketConnectionClosedClientError; - } - - return ErrorCodes::WebSocketConnectionClosedServerError; -} diff --git a/src/realm/sync/network/websocket.hpp b/src/realm/sync/network/websocket.hpp index 1cb518410bf..0c5e21e7788 100644 --- a/src/realm/sync/network/websocket.hpp +++ b/src/realm/sync/network/websocket.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -76,7 +77,7 @@ class Config { /// websocket object is destroyed during execution of the function. virtual bool websocket_text_message_received(const char* data, size_t size); virtual bool websocket_binary_message_received(const char* data, size_t size); - virtual bool websocket_close_message_received(std::error_code error_code, StringData message); + virtual bool websocket_close_message_received(WebSocketError code, std::string_view message); virtual bool websocket_ping_message_received(const char* data, size_t size); virtual bool websocket_pong_message_received(const char* data, size_t size); //@} @@ -221,43 +222,6 @@ const std::error_category& http_error_category() noexcept; std::error_code make_error_code(HttpError) noexcept; -enum class WebSocketError { - websocket_ok = RLM_ERR_WEBSOCKET_OK, - websocket_going_away = RLM_ERR_WEBSOCKET_GOINGAWAY, - websocket_protocol_error = RLM_ERR_WEBSOCKET_PROTOCOLERROR, - websocket_unsupported_data = RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA, - websocket_reserved = RLM_ERR_WEBSOCKET_RESERVED, - websocket_no_status_received = RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED, - websocket_abnormal_closure = RLM_ERR_WEBSOCKET_ABNORMALCLOSURE, - websocket_invalid_payload_data = RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA, - websocket_policy_violation = RLM_ERR_WEBSOCKET_POLICYVIOLATION, - websocket_message_too_big = RLM_ERR_WEBSOCKET_MESSAGETOOBIG, - websocket_invalid_extension = RLM_ERR_WEBSOCKET_INAVALIDEXTENSION, - websocket_internal_server_error = RLM_ERR_WEBSOCKET_INTERNALSERVERERROR, - websocket_tls_handshake_failed = RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED, // Used by default WebSocket - - // WebSocket Errors - reported by server - websocket_unauthorized = RLM_ERR_WEBSOCKET_UNAUTHORIZED, - websocket_forbidden = RLM_ERR_WEBSOCKET_FORBIDDEN, - websocket_moved_permanently = RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY, - websocket_client_too_old = RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD, - websocket_client_too_new = RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW, - websocket_protocol_mismatch = RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH, - - websocket_resolve_failed = RLM_ERR_WEBSOCKET_RESOLVE_FAILED, - websocket_connection_failed = RLM_ERR_WEBSOCKET_CONNECTION_FAILED, - websocket_read_error = RLM_ERR_WEBSOCKET_READ_ERROR, - websocket_write_error = RLM_ERR_WEBSOCKET_WRITE_ERROR, - websocket_retry_error = RLM_ERR_WEBSOCKET_RETRY_ERROR, - websocket_fatal_error = RLM_ERR_WEBSOCKET_FATAL_ERROR, -}; - -const std::error_category& websocket_error_category() noexcept; - -std::error_code make_error_code(WebSocketError) noexcept; - -ErrorCodes::Error get_simplified_websocket_error(WebSocketError); - } // namespace realm::sync::websocket namespace std { @@ -267,9 +231,4 @@ struct is_error_code_enum { static const bool value = true; }; -template <> -struct is_error_code_enum { - static const bool value = true; -}; - } // namespace std diff --git a/src/realm/sync/network/websocket_error.hpp b/src/realm/sync/network/websocket_error.hpp new file mode 100644 index 00000000000..2c0dbbd0bc7 --- /dev/null +++ b/src/realm/sync/network/websocket_error.hpp @@ -0,0 +1,61 @@ +/************************************************************************* + * + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + **************************************************************************/ + + +#pragma once + +#include "realm/error_codes.h" + +#include + +namespace realm::sync::websocket { + +enum class WebSocketError { + websocket_ok = RLM_ERR_WEBSOCKET_OK, + websocket_going_away = RLM_ERR_WEBSOCKET_GOINGAWAY, + websocket_protocol_error = RLM_ERR_WEBSOCKET_PROTOCOLERROR, + websocket_unsupported_data = RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA, + websocket_reserved = RLM_ERR_WEBSOCKET_RESERVED, + websocket_no_status_received = RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED, + websocket_abnormal_closure = RLM_ERR_WEBSOCKET_ABNORMALCLOSURE, + websocket_invalid_payload_data = RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA, + websocket_policy_violation = RLM_ERR_WEBSOCKET_POLICYVIOLATION, + websocket_message_too_big = RLM_ERR_WEBSOCKET_MESSAGETOOBIG, + websocket_invalid_extension = RLM_ERR_WEBSOCKET_INAVALIDEXTENSION, + websocket_internal_server_error = RLM_ERR_WEBSOCKET_INTERNALSERVERERROR, + websocket_tls_handshake_failed = RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED, // Used by default WebSocket + + // WebSocket Errors - reported by server + websocket_unauthorized = RLM_ERR_WEBSOCKET_UNAUTHORIZED, + websocket_forbidden = RLM_ERR_WEBSOCKET_FORBIDDEN, + websocket_moved_permanently = RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY, + websocket_client_too_old = RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD, + websocket_client_too_new = RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW, + websocket_protocol_mismatch = RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH, + + websocket_resolve_failed = RLM_ERR_WEBSOCKET_RESOLVE_FAILED, + websocket_connection_failed = RLM_ERR_WEBSOCKET_CONNECTION_FAILED, + websocket_read_error = RLM_ERR_WEBSOCKET_READ_ERROR, + websocket_write_error = RLM_ERR_WEBSOCKET_WRITE_ERROR, + websocket_retry_error = RLM_ERR_WEBSOCKET_RETRY_ERROR, + websocket_fatal_error = RLM_ERR_WEBSOCKET_FATAL_ERROR, +}; + +std::ostream& operator<<(std::ostream& os, WebSocketError code); + +} // namespace realm::sync::websocket diff --git a/src/realm/sync/noinst/client_impl_base.cpp b/src/realm/sync/noinst/client_impl_base.cpp index d0d77fd5dce..3c41a0504af 100644 --- a/src/realm/sync/noinst/client_impl_base.cpp +++ b/src/realm/sync/noinst/client_impl_base.cpp @@ -499,30 +499,30 @@ void Connection::websocket_error_handler() m_websocket_error_received = true; } -bool Connection::websocket_closed_handler(bool was_clean, Status status) +bool Connection::websocket_closed_handler(bool was_clean, WebSocketError error_code, std::string_view msg) { if (m_force_closed) { logger.debug("Received websocket close message after connection was force closed"); return false; } - logger.info("Closing the websocket with status='%1', was_clean='%2'", status, was_clean); - auto error_code = status.get_std_error_code(); + logger.info("Closing the websocket with error code=%1, message='%2', was_clean=%3", error_code, msg, was_clean); - switch (static_cast(error_code.value())) { + switch (error_code) { case WebSocketError::websocket_ok: break; case WebSocketError::websocket_resolve_failed: [[fallthrough]]; case WebSocketError::websocket_connection_failed: { - constexpr bool try_again = true; - involuntary_disconnect(SessionErrorInfo{std::move(status), try_again}, - ConnectionTerminationReason::connect_operation_failed); // Throws + SessionErrorInfo error_info( + {ErrorCodes::SyncConnectFailed, util::format("Failed to connect to sync: %1", msg)}, IsFatal{false}); + involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::connect_operation_failed); break; } case WebSocketError::websocket_read_error: [[fallthrough]]; case WebSocketError::websocket_write_error: { - read_or_write_error(error_code, status.reason()); // Throws + close_due_to_transient_error({ErrorCodes::ConnectionClosed, msg}, + ConnectionTerminationReason::read_or_write_error); break; } case WebSocketError::websocket_going_away: @@ -540,61 +540,72 @@ bool Connection::websocket_closed_handler(bool was_clean, Status status) case WebSocketError::websocket_no_status_received: [[fallthrough]]; case WebSocketError::websocket_invalid_extension: { - constexpr bool try_again = true; - SessionErrorInfo error_info{std::move(status), try_again}; - involuntary_disconnect(std::move(error_info), - ConnectionTerminationReason::websocket_protocol_violation); // Throws + close_due_to_client_side_error({ErrorCodes::SyncProtocolInvariantFailed, msg}, IsFatal{false}, + ConnectionTerminationReason::websocket_protocol_violation); // Throws break; } case WebSocketError::websocket_message_too_big: { - constexpr bool try_again = true; - auto message = - util::format("Sync websocket closed because the server received a message that was too large: %1", - status.reason()); - SessionErrorInfo error_info(Status(ErrorCodes::LimitExceeded, std::move(message)), try_again); + auto message = util::format( + "Sync websocket closed because the server received a message that was too large: %1", msg); + SessionErrorInfo error_info(Status(ErrorCodes::LimitExceeded, std::move(message)), IsFatal{false}); error_info.server_requests_action = ProtocolErrorInfo::Action::ClientReset; involuntary_disconnect(std::move(error_info), ConnectionTerminationReason::websocket_protocol_violation); // Throws break; } case WebSocketError::websocket_tls_handshake_failed: { - close_due_to_client_side_error(Status(ErrorCodes::TlsHandshakeFailed, status.reason()), IsFatal{false}, + close_due_to_client_side_error(Status(ErrorCodes::TlsHandshakeFailed, msg), IsFatal{false}, ConnectionTerminationReason::ssl_certificate_rejected); // Throws break; } - case WebSocketError::websocket_client_too_old: { - close_due_to_client_side_error(std::move(status), IsFatal{true}, + case WebSocketError::websocket_client_too_old: + [[fallthrough]]; + case WebSocketError::websocket_client_too_new: + [[fallthrough]]; + case WebSocketError::websocket_protocol_mismatch: { + close_due_to_client_side_error({ErrorCodes::SyncProtocolNegotiationFailed, msg}, IsFatal{true}, ConnectionTerminationReason::http_response_says_fatal_error); // Throws break; } - case WebSocketError::websocket_client_too_new: { - close_due_to_client_side_error(std::move(status), IsFatal{true}, - ConnectionTerminationReason::http_response_says_fatal_error); // Throws + case WebSocketError::websocket_fatal_error: { + involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{true}), + ConnectionTerminationReason::http_response_says_fatal_error); break; } - case WebSocketError::websocket_protocol_mismatch: { - close_due_to_client_side_error(std::move(status), IsFatal{true}, - ConnectionTerminationReason::http_response_says_fatal_error); // Throws + case WebSocketError::websocket_forbidden: { + involuntary_disconnect(SessionErrorInfo({ErrorCodes::AuthError, msg}, IsFatal{true}), + ConnectionTerminationReason::http_response_says_fatal_error); break; } - case WebSocketError::websocket_fatal_error: - [[fallthrough]]; - case WebSocketError::websocket_forbidden: { - close_due_to_client_side_error(std::move(status), IsFatal{true}, - ConnectionTerminationReason::http_response_says_fatal_error); // Throws + case WebSocketError::websocket_unauthorized: { + SessionErrorInfo error_info( + {ErrorCodes::AuthError, + util::format("Websocket was closed because of an authentication issue: %1", msg)}, + IsFatal{false}); + error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshUser; + involuntary_disconnect(std::move(error_info), + ConnectionTerminationReason::http_response_says_nonfatal_error); + break; + } + case WebSocketError::websocket_moved_permanently: { + SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}); + error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshLocation; + involuntary_disconnect(std::move(error_info), + ConnectionTerminationReason::http_response_says_nonfatal_error); + break; + } + case WebSocketError::websocket_abnormal_closure: { + SessionErrorInfo error_info({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}); + error_info.server_requests_action = ProtocolErrorInfo::Action::RefreshUser; + involuntary_disconnect(std::move(error_info), + ConnectionTerminationReason::http_response_says_nonfatal_error); break; } - case WebSocketError::websocket_unauthorized: - [[fallthrough]]; - case WebSocketError::websocket_moved_permanently: - [[fallthrough]]; case WebSocketError::websocket_internal_server_error: [[fallthrough]]; - case WebSocketError::websocket_abnormal_closure: - [[fallthrough]]; case WebSocketError::websocket_retry_error: { - close_due_to_client_side_error(error_code, status.reason(), IsFatal{false}, - ConnectionTerminationReason::http_response_says_nonfatal_error); // Throws + involuntary_disconnect(SessionErrorInfo({ErrorCodes::ConnectionClosed, msg}, IsFatal{false}), + ConnectionTerminationReason::http_response_says_nonfatal_error); break; } } @@ -695,13 +706,13 @@ struct Connection::WebSocketObserverShim : public sync::WebSocketObserver { return conn->websocket_binary_message_received(data); } - bool websocket_closed_handler(bool was_clean, Status status) override + bool websocket_closed_handler(bool was_clean, WebSocketError error_code, std::string_view msg) override { if (sentinel->destroyed) { return true; } - return conn->websocket_closed_handler(was_clean, std::move(status)); + return conn->websocket_closed_handler(was_clean, error_code, msg); } }; @@ -782,8 +793,7 @@ void Connection::handle_connect_wait(Status status) REALM_ASSERT_EX(m_state == ConnectionState::connecting, m_state); logger.info("Connect timeout"); // Throws - constexpr bool try_again = true; - involuntary_disconnect(SessionErrorInfo{Status{ErrorCodes::SyncConnectFailed, status.reason()}, try_again}, + involuntary_disconnect(SessionErrorInfo{Status{ErrorCodes::SyncConnectFailed, status.reason()}, IsFatal{false}}, ConnectionTerminationReason::sync_connect_timeout); // Throws } @@ -1117,20 +1127,21 @@ void Connection::close_due_to_client_side_error(std::error_code ec, std::optiona void Connection::close_due_to_client_side_error(Status status, IsFatal is_fatal, ConnectionTerminationReason reason) { logger.info("Connection closed due to error: %1", status); // Throws - const bool try_again = !is_fatal; - involuntary_disconnect(SessionErrorInfo{std::move(status), try_again}, reason); // Throw + involuntary_disconnect(SessionErrorInfo{std::move(status), is_fatal}, reason); // Throw } + void Connection::close_due_to_transient_error(Status status, ConnectionTerminationReason reason) { logger.info("Connection closed due to transient error: %1", status); // Throws - SessionErrorInfo error_info{std::move(status), true}; + SessionErrorInfo error_info{std::move(status), IsFatal{false}}; error_info.server_requests_action = ProtocolErrorInfo::Action::Transient; involuntary_disconnect(std::move(error_info), reason); // Throw } + // Close connection due to error discovered on the server-side, and then // reported to the client by way of a connection-level ERROR message. void Connection::close_due_to_server_side_error(ProtocolError error_code, const ProtocolErrorInfo& info) @@ -1138,8 +1149,8 @@ void Connection::close_due_to_server_side_error(ProtocolError error_code, const logger.info("Connection closed due to error reported by server: %1 (%2)", info.message, int(error_code)); // Throws - const auto reason = info.try_again ? ConnectionTerminationReason::server_said_try_again_later - : ConnectionTerminationReason::server_said_do_not_reconnect; + const auto reason = info.is_fatal ? ConnectionTerminationReason::server_said_do_not_reconnect + : ConnectionTerminationReason::server_said_try_again_later; involuntary_disconnect(SessionErrorInfo{info, protocol_error_to_status(error_code, info.message)}, reason); // Throws } @@ -1284,8 +1295,8 @@ void Connection::receive_error_message(const ProtocolErrorInfo& info, session_id return; } - logger.info("Received: ERROR \"%1\" (error_code=%2, try_again=%3, session_ident=%4, error_action=%5)", - info.message, info.raw_error_code, info.try_again, session_ident, + logger.info("Received: ERROR \"%1\" (error_code=%2, is_fatal=%3, session_ident=%4, error_action=%5)", + info.message, info.raw_error_code, info.is_fatal, session_ident, info.server_requests_action); // Throws bool known_error_code = bool(get_protocol_error_message(info.raw_error_code)); @@ -1563,9 +1574,8 @@ void Session::on_integration_failure(const IntegrationException& error) m_client_error = util::make_optional(error); m_error_to_send = true; - constexpr bool try_again = true; // Surface the error to the user otherwise is lost. - on_connection_state_changed(m_conn.get_state(), SessionErrorInfo{error.to_status(), try_again}); + on_connection_state_changed(m_conn.get_state(), SessionErrorInfo{error.to_status(), IsFatal{false}}); // Since the deactivation process has not been initiated, the UNBIND // message cannot have been sent unless an ERROR message was received. @@ -1689,7 +1699,7 @@ void Session::activate() logger.error("Error integrating bootstrap changesets: %1", error.what()); m_suspended = true; m_conn.one_less_active_unsuspended_session(); // Throws - on_suspended(SessionErrorInfo{Status{error.code(), error.what()}, false}); + on_suspended(SessionErrorInfo{Status{error.code(), error.what()}, IsFatal{true}}); } if (has_pending_client_reset) { @@ -2297,12 +2307,13 @@ Status Session::receive_ident_message(SaltedFileIdent client_file_ident) catch (const std::exception& e) { auto err_msg = util::format("A fatal error occurred during client reset: '%1'", e.what()); logger.error(err_msg.c_str()); - SessionErrorInfo err_info(Status{ErrorCodes::AutoClientResetFailed, err_msg}, false); + SessionErrorInfo err_info(Status{ErrorCodes::AutoClientResetFailed, err_msg}, IsFatal{true}); suspend(err_info); return Status::OK(); } if (!did_client_reset) { - repl.get_history().set_client_file_ident(client_file_ident, m_fix_up_object_ids); // Throws + repl.get_history().set_client_file_ident(client_file_ident, + m_fix_up_object_ids); // Throws m_progress.download.last_integrated_client_version = 0; m_progress.upload.client_version = 0; m_last_version_selected_for_upload = 0; @@ -2334,8 +2345,7 @@ Status Session::receive_download_message(const SyncProgress& progress, std::uint progress.download.server_version, progress.download.last_integrated_client_version, progress.latest_server_version.version, progress.latest_server_version.salt, progress.upload.client_version, progress.upload.last_integrated_server_version, downloadable_bytes, - batch_state != DownloadBatchState::MoreToCome, query_version, - received_changesets.size()); // Throws + batch_state != DownloadBatchState::MoreToCome, query_version, received_changesets.size()); // Throws // Ignore download messages when the client detects an error. This is to prevent transforming the same bad // changeset over and over again. @@ -2498,8 +2508,8 @@ Status Session::receive_query_error_message(int error_code, std::string_view mes // deactivated upon return. Status Session::receive_error_message(const ProtocolErrorInfo& info) { - logger.info("Received: ERROR \"%1\" (error_code=%2, try_again=%3, error_action=%4)", info.message, - info.raw_error_code, info.try_again, info.server_requests_action); // Throws + logger.info("Received: ERROR \"%1\" (error_code=%2, is_fatal=%3, error_action=%4)", info.message, + info.raw_error_code, info.is_fatal, info.server_requests_action); // Throws bool legal_at_this_time = (m_bind_message_sent && !m_error_message_received && !m_unbound_message_received); if (REALM_UNLIKELY(!legal_at_this_time)) { @@ -2571,7 +2581,7 @@ void Session::suspend(const SessionErrorInfo& info) on_suspended(info); // Throws } - if (info.try_again) { + if (!info.is_fatal) { begin_resumption_delay(info); } diff --git a/src/realm/sync/noinst/client_impl_base.hpp b/src/realm/sync/noinst/client_impl_base.hpp index 3152b10bb3a..b92f04932b9 100644 --- a/src/realm/sync/noinst/client_impl_base.hpp +++ b/src/realm/sync/noinst/client_impl_base.hpp @@ -483,7 +483,7 @@ class ClientImpl::Connection { void websocket_connected_handler(const std::string& protocol); bool websocket_binary_message_received(util::Span data); void websocket_error_handler(); - bool websocket_closed_handler(bool, Status); + bool websocket_closed_handler(bool, websocket::WebSocketError, std::string_view msg); connection_ident_type get_ident() const noexcept; const ServerEndpoint& get_server_endpoint() const noexcept; @@ -512,9 +512,6 @@ class ClientImpl::Connection { }; struct WebSocketObserverShim; - class IsFatalTag {}; - using IsFatal = util::TaggedBool; - using ReceivedChangesets = ClientProtocol::ReceivedChangesets; template @@ -1312,8 +1309,7 @@ void ClientImpl::Connection::for_each_active_session(H handler) inline void ClientImpl::Connection::voluntary_disconnect() { m_reconnect_info.update(ConnectionTerminationReason::closed_voluntarily, std::nullopt); - constexpr bool try_again = true; - SessionErrorInfo error_info{Status{ErrorCodes::ConnectionClosed, "Connection closed"}, try_again}; + SessionErrorInfo error_info{Status{ErrorCodes::ConnectionClosed, "Connection closed"}, IsFatal{false}}; error_info.server_requests_action = ProtocolErrorInfo::Action::Transient; disconnect(std::move(error_info)); // Throws diff --git a/src/realm/sync/noinst/protocol_codec.hpp b/src/realm/sync/noinst/protocol_codec.hpp index df4ffa60b1b..d398bb43276 100644 --- a/src/realm/sync/noinst/protocol_codec.hpp +++ b/src/realm/sync/noinst/protocol_codec.hpp @@ -252,7 +252,7 @@ class ClientProtocol { else if (message_type == "error") { auto error_code = msg.read_next(); auto message_size = msg.read_next(); - auto try_again = msg.read_next(); + auto is_fatal = sync::IsFatal{!msg.read_next()}; auto session_ident = msg.read_next('\n'); bool unknown_error = !sync::get_protocol_error_message(error_code); @@ -262,7 +262,7 @@ class ClientProtocol { auto message = msg.read_sized_data(message_size); - connection.receive_error_message(sync::ProtocolErrorInfo{error_code, message, try_again}, + connection.receive_error_message(sync::ProtocolErrorInfo{error_code, message, is_fatal}, session_ident); // Throws } else if (message_type == "json_error") { // introduced in protocol 4 @@ -275,7 +275,7 @@ class ClientProtocol { auto json = nlohmann::json::parse(json_raw); logger.trace("Error message encoded as json: %1", json_raw); info.client_reset_recovery_is_disabled = json["isRecoveryModeDisabled"]; - info.try_again = json["tryAgain"]; + info.is_fatal = sync::IsFatal{!json["tryAgain"]}; info.message = json["message"]; info.log_url = std::make_optional(json["logURL"]); info.should_client_reset = std::make_optional(json["shouldClientReset"]); @@ -504,6 +504,8 @@ class ClientProtocol { {"ClientResetNoRecovery", action::ClientResetNoRecovery}, {"MigrateToFLX", action::MigrateToFLX}, {"RevertToPBS", action::RevertToPBS}, + {"RefreshUser", action::RefreshUser}, + {"RefreshLocation", action::RefreshLocation}, }; if (auto action_it = mapping.find(action_string); action_it != mapping.end()) { diff --git a/src/realm/sync/protocol.hpp b/src/realm/sync/protocol.hpp index beb4bc398b4..6d0e25b2b12 100644 --- a/src/realm/sync/protocol.hpp +++ b/src/realm/sync/protocol.hpp @@ -7,6 +7,7 @@ #include #include #include +#include // NOTE: The protocol specification is in `/doc/protocol.md` @@ -247,6 +248,9 @@ struct ResumptionDelayInfo { int delay_jitter_divisor = 4; }; +class IsFatalTag {}; +using IsFatal = util::TaggedBool; + struct ProtocolErrorInfo { enum class Action { NoAction, @@ -258,14 +262,18 @@ struct ProtocolErrorInfo { ClientReset, ClientResetNoRecovery, MigrateToFLX, - RevertToPBS + RevertToPBS, + // The RefreshUser/RefreshLocation actions are currently generated internally when the + // sync websocket is closed with specific error codes. + RefreshUser, + RefreshLocation, }; ProtocolErrorInfo() = default; - ProtocolErrorInfo(int error_code, const std::string& msg, bool do_try_again) + ProtocolErrorInfo(int error_code, const std::string& msg, IsFatal is_fatal) : raw_error_code(error_code) , message(msg) - , try_again(do_try_again) + , is_fatal(is_fatal) , client_reset_recovery_is_disabled(false) , should_client_reset(util::none) , server_requests_action(Action::NoAction) @@ -273,7 +281,7 @@ struct ProtocolErrorInfo { } int raw_error_code = 0; std::string message; - bool try_again = false; + IsFatal is_fatal = IsFatal{true}; bool client_reset_recovery_is_disabled = false; std::optional should_client_reset; std::optional log_url; @@ -283,11 +291,6 @@ struct ProtocolErrorInfo { std::optional resumption_delay_interval; Action server_requests_action; std::optional migration_query_string; - - bool is_fatal() const - { - return !try_again; - } }; @@ -447,6 +450,10 @@ inline std::ostream& operator<<(std::ostream& o, ProtocolErrorInfo::Action actio return o << "MigrateToFLX"; case ProtocolErrorInfo::Action::RevertToPBS: return o << "RevertToPBS"; + case ProtocolErrorInfo::Action::RefreshUser: + return o << "RefreshUser"; + case ProtocolErrorInfo::Action::RefreshLocation: + return o << "RefreshLocation"; } return o << "Invalid error action: " << int64_t(action); } diff --git a/src/realm/sync/socket_provider.hpp b/src/realm/sync/socket_provider.hpp index 265143c0331..f2c6b3be62f 100644 --- a/src/realm/sync/socket_provider.hpp +++ b/src/realm/sync/socket_provider.hpp @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -238,15 +239,16 @@ struct WebSocketObserver { /// /// @param was_clean Was the TCP connection closed after the WebSocket closing /// handshake was completed. - /// @param status A Status object containing the WebSocket status code and the - /// reason string why the connection was closed. + /// @param error_code The error code received or synthesized when the websocket was closed. + /// @param message The message received in the close frame when the websocket was closed. /// /// @return bool designates whether the WebSocket object has been destroyed /// during the execution of this function. The normal return value is /// True to indicate the WebSocket object is no longer valid. If False /// is returned, the WebSocket object will be destroyed at some point /// in the future. - virtual bool websocket_closed_handler(bool was_clean, Status status) = 0; + virtual bool websocket_closed_handler(bool was_clean, websocket::WebSocketError error_code, + std::string_view message) = 0; }; } // namespace realm::sync diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 465dfa3216f..e68a9ea9e34 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -740,16 +740,6 @@ TEST_CASE("C API (non-database)", "[c_api]") { CHECK(ec_check.category() == std::system_category()); CHECK(ec_check.value() == int(error_code.value())); - error_code.assign(ErrorCodes::WebSocketResolveFailedError, - realm::sync::websocket::websocket_error_category()); - error = c_api::to_capi(SystemError(error_code, "").to_status(), message); - CHECK(error.category == realm_sync_error_category_e::RLM_SYNC_ERROR_CATEGORY_WEBSOCKET); - CHECK(error.value == realm_errno::RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR); - - c_api::sync_error_to_error_code(error, &ec_check); - CHECK(ec_check.category() == realm::sync::websocket::websocket_error_category()); - CHECK(ec_check.value() == int(error_code.value())); - error_code = make_error_code(util::error::misc_errors::unknown); error = c_api::to_capi(SystemError(error_code, "").to_status(), message); CHECK(error.category == realm_sync_error_category_e::RLM_SYNC_ERROR_CATEGORY_UNKNOWN); @@ -6208,9 +6198,9 @@ TEST_CASE("C API app: websocket provider", "[sync][app][c_api][baas]") { return m_observer->websocket_binary_message_received(data); } - bool websocket_closed_handler(bool was_clean, Status status) override + bool websocket_closed_handler(bool was_clean, WebSocketError error, std::string_view msg) override { - return m_observer->websocket_closed_handler(was_clean, std::move(status)); + return m_observer->websocket_closed_handler(was_clean, error, msg); } private: @@ -6278,7 +6268,7 @@ TEST_CASE("C API app: websocket provider", "[sync][app][c_api][baas]") { auto test_data = static_cast(userdata); REQUIRE(test_data); auto cb = [callback_copy = callback](Status s) { - realm_sync_socket_callback_complete(callback_copy, static_cast(s.code()), + realm_sync_socket_callback_complete(callback_copy, static_cast(s.code()), s.reason().c_str()); }; test_data->socket_provider->post(std::move(cb)); diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index 34c3870668a..a972b836196 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -228,10 +228,6 @@ TEST_CASE("sync: pending client resets are cleared when downloads are complete", SyncTestFile realm_config(app->current_user(), partition.value, schema); realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover; realm_config.sync_config->error_handler = [&](std::shared_ptr, SyncError err) { - if (err.get_system_error() == sync::websocket::WebSocketError::websocket_read_error) { - return; - } - if (err.server_requests_action == sync::ProtocolErrorInfo::Action::Warning || err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient) { return; @@ -902,7 +898,7 @@ TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") { REQUIRE(session); } sync::SessionErrorInfo synthetic(Status{ErrorCodes::SyncClientResetRequired, "A fake client reset error"}, - false); + sync::IsFatal{true}); synthetic.server_requests_action = sync::ProtocolErrorInfo::Action::ClientReset; SyncSession::OnlyForTesting::handle_error(*session, std::move(synthetic)); diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index 9c479b06669..d33aa24c8b0 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -401,7 +401,7 @@ TEST_CASE("sync: error handling", "[sync][session]") { SECTION("Doesn't treat unknown system errors as being fatal") { std::error_code code = std::error_code{EBADF, std::generic_category()}; - sync::SessionErrorInfo err{Status{code, "Not a real error message"}, true}; + sync::SessionErrorInfo err{Status{code, "Not a real error message"}, sync::IsFatal{false}}; err.server_requests_action = ProtocolErrorInfo::Action::Transient; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); CHECK(!sessions_are_inactive(*session)); @@ -431,7 +431,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { } sync::SessionErrorInfo initial_error{ - Status{std::error_code{code, realm::sync::protocol_error_category()}, "Something bad happened"}, true}; + Status{std::error_code{code, realm::sync::protocol_error_category()}, "Something bad happened"}, + sync::IsFatal{false}}; initial_error.server_requests_action = ProtocolErrorInfo::Action::ClientReset; std::time_t just_before_raw = std::time(nullptr); SyncSession::OnlyForTesting::handle_error(*session, std::move(initial_error)); @@ -476,7 +477,6 @@ struct RegularUser { TEMPLATE_TEST_CASE("sync: stop policy behavior", "[sync][session]", RegularUser) { - using ProtocolError = realm::sync::ProtocolError; const std::string dummy_auth_url = "https://realm.example.org"; if (!EventLoop::has_implementation()) return; @@ -551,9 +551,8 @@ TEMPLATE_TEST_CASE("sync: stop policy behavior", "[sync][session]", RegularUser) } SECTION("transitions to Inactive if a fatal error occurs") { - std::error_code code = - std::error_code{static_cast(ProtocolError::bad_syntax), realm::sync::protocol_error_category()}; - sync::SessionErrorInfo err{Status{code, "Not a real error message"}, false}; + sync::SessionErrorInfo err{Status{ErrorCodes::SyncProtocolInvariantFailed, "Not a real error message"}, + sync::IsFatal{true}}; err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::ProtocolViolation; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); CHECK(sessions_are_inactive(*session)); @@ -563,9 +562,8 @@ TEMPLATE_TEST_CASE("sync: stop policy behavior", "[sync][session]", RegularUser) SECTION("ignores non-fatal errors and does not transition to Inactive") { // Fire a simulated *non-fatal* error. - std::error_code code = - std::error_code{static_cast(ProtocolError::other_error), realm::sync::protocol_error_category()}; - sync::SessionErrorInfo err{Status{code, "Not a real error message"}, true}; + sync::SessionErrorInfo err{Status{ErrorCodes::ConnectionClosed, "Not a real error message"}, + sync::IsFatal{false}}; err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::Transient; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); REQUIRE(session->state() == SyncSession::State::Dying); diff --git a/test/object-store/sync/session/wait_for_completion.cpp b/test/object-store/sync/session/wait_for_completion.cpp index 9f4ec49ae81..1ad20b72e0a 100644 --- a/test/object-store/sync/session/wait_for_completion.cpp +++ b/test/object-store/sync/session/wait_for_completion.cpp @@ -93,23 +93,21 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio } SECTION("aborts properly when queued and the session errors out") { - using ProtocolError = realm::sync::ProtocolError; auto user = sync_manager->get_user("user-async-wait-download-4", ENCODE_FAKE_JWT("not_a_real_token"), ENCODE_FAKE_JWT("not_a_real_token"), dummy_auth_url, dummy_device_id); std::atomic error_count(0); std::shared_ptr session = sync_session(user, "/async-wait-download-4", [&](auto, auto) { ++error_count; }); - std::error_code code = - std::error_code{static_cast(ProtocolError::bad_syntax), realm::sync::protocol_error_category()}; // Register the download-completion notification session->wait_for_download_completion([&](Status status) { - REQUIRE(status.get_std_error_code() == code); + REQUIRE(status == ErrorCodes::SyncProtocolInvariantFailed); handler_called = true; }); REQUIRE(handler_called == false); // Now trigger an error - sync::SessionErrorInfo err{Status{code, "Not a real error message"}, false}; + sync::SessionErrorInfo err{Status{ErrorCodes::SyncProtocolInvariantFailed, "Not a real error message"}, + sync::IsFatal{false}}; err.server_requests_action = sync::ProtocolErrorInfo::Action::ProtocolViolation; SyncSession::OnlyForTesting::handle_error(*session, std::move(err)); EventLoop::main().run_until([&] { diff --git a/test/sync_fixtures.hpp b/test/sync_fixtures.hpp index d683448cb24..e1fb6910ad7 100644 --- a/test/sync_fixtures.hpp +++ b/test/sync_fixtures.hpp @@ -593,7 +593,7 @@ class MultiClientServerFixture { if (state != ConnectionState::disconnected) return; REALM_ASSERT(error_info); - handler(error_info->status, error_info->is_fatal(), error_info->message); + handler(error_info->status, error_info->is_fatal, error_info->message); }; m_connection_state_change_listeners[client_index] = std::move(handler_wrapped); } @@ -713,7 +713,7 @@ class MultiClientServerFixture { REALM_ASSERT(error); unit_test::TestContext& test_context = m_test_context; test_context.logger->error("Client disconnect: %1: %2 (is_fatal=%3)", - error->status.get_std_error_code(), error->message, error->is_fatal()); + error->status.get_std_error_code(), error->message, error->is_fatal); bool client_error_occurred = true; CHECK_NOT(client_error_occurred); stop(); @@ -1078,7 +1078,7 @@ inline void RealmFixture::setup_error_handler(util::UniqueFunction if (state != ConnectionState::disconnected) return; REALM_ASSERT(error_info); - handler(error_info->status, error_info->is_fatal(), error_info->message); + handler(error_info->status, error_info->is_fatal, error_info->message); }; m_session.set_connection_state_change_listener(std::move(listener)); } diff --git a/test/test_sync.cpp b/test/test_sync.cpp index cfd950e1205..109eb7ef082 100644 --- a/test/test_sync.cpp +++ b/test/test_sync.cpp @@ -147,9 +147,8 @@ TEST(Sync_BadVirtualPath) return; REALM_ASSERT(error_info); std::error_code ec = error_info->status.get_std_error_code(); - bool is_fatal = error_info->is_fatal(); CHECK_EQUAL(sync::ProtocolError::illegal_realm_path, ec); - CHECK(is_fatal); + CHECK(error_info->is_fatal); ++nerrors; if (nerrors == 3) fixture.stop(); @@ -786,7 +785,7 @@ struct ExpectChangesetError { return; REALM_ASSERT(error_info); CHECK_EQUAL(error_info->status, ErrorCodes::BadChangeset); - CHECK(!error_info->is_fatal()); + CHECK(!error_info->is_fatal); CHECK_EQUAL(error_info->message, "Failed to transform received changeset: Schema mismatch: " + expected_error); fixture.stop(); @@ -4574,9 +4573,7 @@ TEST(Sync_ServerDiscardDeadConnections) BowlOfStonesSemaphore bowl; auto error_handler = [&](Status status, bool, const std::string&) { - auto ec = status.get_std_error_code(); - bool valid_error = (ec == sync::websocket::WebSocketError::websocket_read_error); - CHECK(valid_error); + CHECK_EQUAL(status, ErrorCodes::ConnectionClosed); bowl.add_stone(); }; fixture.set_client_side_error_handler(std::move(error_handler)); @@ -5172,7 +5169,7 @@ TEST_IF(Sync_SSL_Certificates, false) CHECK(error_info); client_logger->debug( "State change: disconnected, error_code = %1, is_fatal = %2, detailed_message = %3", - error_info->status.get_std_error_code(), error_info->is_fatal(), error_info->message); + error_info->status.get_std_error_code(), error_info->is_fatal, error_info->message); // We expect to get through the SSL handshake but will hit an error due to the wrong token. CHECK_NOT_EQUAL(error_info->status, ErrorCodes::TlsHandshakeFailed); client.shutdown(); @@ -5249,7 +5246,7 @@ TEST(Sync_BadChangeset) return; REALM_ASSERT(error_info); std::error_code ec = error_info->status.get_std_error_code(); - bool is_fatal = error_info->is_fatal(); + bool is_fatal = error_info->is_fatal; CHECK_EQUAL(sync::ProtocolError::bad_changeset, ec); CHECK(is_fatal); did_fail = true; diff --git a/test/test_util_websocket.cpp b/test/test_util_websocket.cpp index 7f2a5ad4e9d..752340c9d5c 100644 --- a/test/test_util_websocket.cpp +++ b/test/test_util_websocket.cpp @@ -171,7 +171,7 @@ class WSConfig : public websocket::Config { std::vector text_messages; std::vector binary_messages; - std::vector> close_messages; + std::vector> close_messages; std::vector ping_messages; std::vector pong_messages; @@ -244,7 +244,8 @@ class WSConfig : public websocket::Config { return true; } - bool websocket_close_message_received(std::error_code error_code, StringData error_message) override + bool websocket_close_message_received(websocket::WebSocketError error_code, + std::string_view error_message) override { close_messages.push_back(std::make_pair(error_code, std::string{error_message})); return true; @@ -389,7 +390,7 @@ TEST(WebSocket_Messages) "close message", 15, handler_no_op); CHECK_EQUAL(config_1.close_messages.size(), 1); - CHECK_EQUAL(config_1.close_messages[0].first.value(), 1000); + CHECK_EQUAL(static_cast(config_1.close_messages[0].first), 1000); CHECK_EQUAL(config_1.close_messages[0].second, "close message"); std::vector message_sizes{1, 2, 100, 125, 126, 127, 128, 200, 1000, 65000, 65535, 65536, 100000, 1000000};