diff --git a/src/signalrclient/hub_connection_impl.cpp b/src/signalrclient/hub_connection_impl.cpp index 935eba6..3f1886d 100644 --- a/src/signalrclient/hub_connection_impl.cpp +++ b/src/signalrclient/hub_connection_impl.cpp @@ -409,8 +409,23 @@ namespace signalr } break; case message_type::close: - // TODO - break; + { + auto close = static_cast(val.get()); + if (m_logger.is_enabled(trace_level::debug)) + { + if (close->error.length() == 0) + { + m_logger.log(trace_level::debug, "received close message."); + } + else + { + m_logger.log(trace_level::debug, "received close message with an error: " + close->error); + } + + } + m_connection->stop([](std::exception_ptr) {}, std::make_exception_ptr(std::runtime_error("the server closed the connection with the following error: " + close->error))); + return; + } default: throw std::runtime_error("unknown message type '" + std::to_string(static_cast(val->message_type)) + "' received"); break; diff --git a/src/signalrclient/hub_protocol.h b/src/signalrclient/hub_protocol.h index a35a9a1..b67f15c 100644 --- a/src/signalrclient/hub_protocol.h +++ b/src/signalrclient/hub_protocol.h @@ -61,6 +61,14 @@ namespace signalr bool has_result; }; + struct close_message : hub_message + { + close_message(std::string&& error, bool allowReconnect) : hub_message(signalr::message_type::close), error(error), allowReconnect(allowReconnect) {} + + std::string error; + bool allowReconnect; + }; + struct ping_message : hub_message { ping_message() : hub_message(signalr::message_type::ping) {} diff --git a/src/signalrclient/json_hub_protocol.cpp b/src/signalrclient/json_hub_protocol.cpp index 52280eb..1e51837 100644 --- a/src/signalrclient/json_hub_protocol.cpp +++ b/src/signalrclient/json_hub_protocol.cpp @@ -216,6 +216,31 @@ namespace signalr hub_message = std::unique_ptr(new ping_message()); break; } + case message_type::close: + { + found = obj.find("error"); + std::string error; + if (found != obj.end()) + { + if (!found->second.is_string()) + { + throw signalr_exception("Expected 'error' to be of type 'string'"); + } + error = found->second.as_string(); + } + bool allowReconnect = false; + found = obj.find("allowReconnect"); + if (found != obj.end()) + { + if (!found->second.is_bool()) + { + throw signalr_exception("Expected 'allowReconnect' to be of type 'bool'"); + } + allowReconnect = found->second.as_bool(); + } + hub_message = std::unique_ptr(new close_message(std::move(error), allowReconnect)); + break; + } // TODO: other message types default: // Future protocol changes can add message types, old clients can ignore them diff --git a/src/signalrclient/messagepack_hub_protocol.cpp b/src/signalrclient/messagepack_hub_protocol.cpp index 3f5e25a..5d1cf93 100644 --- a/src/signalrclient/messagepack_hub_protocol.cpp +++ b/src/signalrclient/messagepack_hub_protocol.cpp @@ -433,6 +433,36 @@ namespace signalr vec.emplace_back(std::unique_ptr(new ping_message())); break; } + case message_type::close: + { + std::string error; + if (msgpack_obj_index->type == msgpack::type::NIL) + { + // Empty string + } + else if (msgpack_obj_index->type != msgpack::type::STR) + { + throw signalr_exception("reading 'error' as string failed"); + } + else + { + error.append(msgpack_obj_index->via.str.ptr, msgpack_obj_index->via.str.size); + } + ++msgpack_obj_index; + + bool allowReconnect = false; + if (num_elements_of_message > 2) + { + if (msgpack_obj_index->type != msgpack::type::BOOLEAN) + { + throw signalr_exception("reading 'allowReconnect' as bool failed"); + } + allowReconnect = msgpack_obj_index->via.boolean; + } + + vec.push_back(std::unique_ptr(new close_message(std::move(error), allowReconnect))); + break; + } // TODO: other message types default: // Future protocol changes can add message types, old clients can ignore them diff --git a/test/signalrclienttests/hub_connection_tests.cpp b/test/signalrclienttests/hub_connection_tests.cpp index 8188ffa..feecb7a 100644 --- a/test/signalrclienttests/hub_connection_tests.cpp +++ b/test/signalrclienttests/hub_connection_tests.cpp @@ -2182,4 +2182,40 @@ TEST(receive, unknown_message_type_closes_connection) { ASSERT_STREQ("unknown message type '100' received", ex.what()); } +} + +TEST(receive, close_message_closes_connection) +{ + auto websocket_client = create_test_websocket_client(); + auto hub_connection = create_hub_connection(websocket_client); + + auto disconnect_mre = manual_reset_event(); + hub_connection.set_disconnected([&disconnect_mre](std::exception_ptr ex) + { + disconnect_mre.set(ex); + }); + + auto mre = manual_reset_event(); + hub_connection.start([&mre](std::exception_ptr exception) + { + mre.set(exception); + }); + + ASSERT_FALSE(websocket_client->receive_loop_started.wait(5000)); + ASSERT_FALSE(websocket_client->handshake_sent.wait(5000)); + websocket_client->receive_message("{ }\x1e"); + + mre.get(); + + websocket_client->receive_message("{ \"type\": 7, \"error\":\"custom error from server\" }\x1e"); + + try + { + disconnect_mre.get(); + ASSERT_TRUE(false); + } + catch (const std::exception& ex) + { + ASSERT_STREQ("the server closed the connection with the following error: custom error from server", ex.what()); + } } \ No newline at end of file diff --git a/test/signalrclienttests/json_hub_protocol_tests.cpp b/test/signalrclienttests/json_hub_protocol_tests.cpp index a0192a0..4610794 100644 --- a/test/signalrclienttests/json_hub_protocol_tests.cpp +++ b/test/signalrclienttests/json_hub_protocol_tests.cpp @@ -98,6 +98,35 @@ TEST(json_hub_protocol, parsing_field_order_does_not_matter) assert_hub_message_equality(&message, output[0].get()); } +std::vector>> close_messages +{ + // close message with error + { "{\"error\":\"error\",\"type\":7}\x1e", + std::shared_ptr(new close_message("error", false)) }, + + // close message without error + { "{\"type\":7}\x1e", + std::shared_ptr(new close_message("", false)) }, + + // close message with error and reconnect + { "{\"error\":\"error\",\"allowReconnect\":true,\"type\":7}\x1e", + std::shared_ptr(new close_message("error", true)) }, + + // close message with extra property + { "{\"error\":\"error\",\"extra\":true,\"type\":7}\x1e", + std::shared_ptr(new close_message("error", false)) }, +}; + +TEST(json_hub_protocol, can_parse_close_message) +{ + for (auto& data : close_messages) + { + auto output = json_hub_protocol().parse_messages(data.first); + ASSERT_EQ(1, output.size()); + assert_hub_message_equality(data.second.get(), output[0].get()); + } +} + TEST(json_hub_protocol, can_serialize_binary) { auto output = json_hub_protocol().write_message( @@ -146,16 +175,22 @@ std::vector> invalid_messages { "{\"arguments\":[],\"target\":\"send\",\"invocationId\":42}\x1e", "Field 'type' not found" }, + // invocation message { "{\"type\":1}\x1e", "Field 'target' not found for 'invocation' message" }, { "{\"type\":1,\"target\":\"send\",\"invocationId\":42}\x1e", "Field 'arguments' not found for 'invocation' message" }, { "{\"type\":1,\"target\":\"send\",\"arguments\":[],\"invocationId\":42}\x1e", "Expected 'invocationId' to be of type 'string'" }, { "{\"type\":1,\"target\":\"send\",\"arguments\":42,\"invocationId\":\"42\"}\x1e", "Expected 'arguments' to be of type 'array'" }, { "{\"type\":1,\"target\":true,\"arguments\":[],\"invocationId\":\"42\"}\x1e", "Expected 'target' to be of type 'string'" }, + // completion message { "{\"type\":3}\x1e", "Field 'invocationId' not found for 'completion' message" }, { "{\"type\":3,\"invocationId\":42}\x1e", "Expected 'invocationId' to be of type 'string'" }, { "{\"type\":3,\"invocationId\":\"42\",\"error\":[]}\x1e", "Expected 'error' to be of type 'string'" }, { "{\"type\":3,\"invocationId\":\"42\",\"error\":\"foo\",\"result\":true}\x1e", "The 'error' and 'result' properties are mutually exclusive." }, + + // close message + { "{\"type\":7,\"error\":32}\x1e", "Expected 'error' to be of type 'string'"}, + { "{\"type\":7,\"allowReconnect\":\"q\"}\x1e", "Expected 'allowReconnect' to be of type 'bool'"}, }; TEST(json_hub_protocol, invalid_messages_throw) diff --git a/test/signalrclienttests/messagepack_hub_protocol_tests.cpp b/test/signalrclienttests/messagepack_hub_protocol_tests.cpp index 127dd86..21bc31a 100644 --- a/test/signalrclienttests/messagepack_hub_protocol_tests.cpp +++ b/test/signalrclienttests/messagepack_hub_protocol_tests.cpp @@ -96,6 +96,38 @@ TEST(messagepack_hub_protocol, parse_message) } } +namespace +{ + std::vector>> close_messages + { + // close message with error + { string_from_bytes({0x08, 0x92, 0x07, 0xA5, 0x65, 0x72, 0x72, 0x6F, 0x72}), + std::shared_ptr(new close_message("error", false)) }, + + // close message without error + { string_from_bytes({0x03, 0x92, 0x07, 0xC0}), + std::shared_ptr(new close_message("", false)) }, + + // close message with error and reconnect + { string_from_bytes({0x09, 0x93, 0x07, 0xA5, 0x65, 0x72, 0x72, 0x6F, 0x72, 0xC3}), + std::shared_ptr(new close_message("error", true)) }, + + // close message with extra property + { string_from_bytes({0x0A, 0x94, 0x07, 0xA5, 0x65, 0x72, 0x72, 0x6F, 0x72, 0xC2, 0x80}), + std::shared_ptr(new close_message("error", false)) }, + }; +} + +TEST(messagepack_hub_protocol, can_parse_close_message) +{ + for (auto& data : close_messages) + { + auto output = messagepack_hub_protocol().parse_messages(data.first); + ASSERT_EQ(1, output.size()); + assert_hub_message_equality(data.second.get(), output[0].get()); + } +} + TEST(messagepack_hub_protocol, can_parse_multiple_messages) { auto payload = string_from_bytes({ 0x0D, 0x96, 0x01, 0x80, 0xC0, 0xA6, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x90, 0x90, @@ -152,6 +184,10 @@ namespace { string_from_bytes({0x05, 0x93, 0x03, 0x80, 0xA1, 0x31}), "completion message has too few properties"}, { string_from_bytes({0x06, 0x94, 0x03, 0x80, 0xA1, 0x31, 0x03}), "completion message has too few properties"}, { string_from_bytes({0x08, 0x95, 0x03, 0x80, 0xA1, 0x31, 0x01, 0x91, 0x03}), "reading 'error' as string failed"}, + + // close message + { string_from_bytes({0x03, 0x92, 0x07, 0xC3}), "reading 'error' as string failed"}, + { string_from_bytes({0x04, 0x93, 0x07, 0xA0, 0xA0}), "reading 'allowReconnect' as bool failed"}, }; } diff --git a/test/signalrclienttests/test_utils.cpp b/test/signalrclienttests/test_utils.cpp index 7ec7aa4..3ab1af6 100644 --- a/test/signalrclienttests/test_utils.cpp +++ b/test/signalrclienttests/test_utils.cpp @@ -189,6 +189,15 @@ void assert_hub_message_equality(signalr::hub_message* expected, signalr::hub_me // No fields on ping messages currently break; } + case message_type::close: + { + auto expected_message = reinterpret_cast(expected); + auto actual_message = reinterpret_cast(actual); + + ASSERT_STREQ(expected_message->error.data(), actual_message->error.data()); + ASSERT_EQ(expected_message->allowReconnect, actual_message->allowReconnect); + break; + } default: ASSERT_TRUE(false); break;