From 3a7973db023ed1f53e1b8528b178b38099199fcc Mon Sep 17 00:00:00 2001 From: moozzyk Date: Sat, 1 Jul 2017 08:33:45 -0700 Subject: [PATCH 1/3] Adding result kind to completion message Before we would rely on error being null to detect whether to read results and we had an additional 'hasResult' field. Now all this information is codified in a field. --- .../Protocol/MessagePackHubProtocol.cs | 68 ++++++++++++++----- .../Protocol/MessagePackHubProtocolTests.cs | 23 ++++--- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index 12b9269153..cdfc0f0e16 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -16,6 +16,10 @@ public class MessagePackHubProtocol : IHubProtocol private const int StreamItemMessageType = 2; private const int CompletionMessageType = 3; + private const int ErrorResult = 1; + private const int VoidResult = 2; + private const int NonVoidResult = 3; + public string Name => "messagepack"; public ProtocolType Type => ProtocolType.Binary; @@ -38,10 +42,7 @@ public bool TryParseMessages(ReadOnlyBuffer input, IInvocationBinder binde private static HubMessage ParseMessage(Stream input, IInvocationBinder binder) { var unpacker = Unpacker.Create(input); - if (!unpacker.ReadInt32(out var messageType)) - { - throw new FormatException("Message type is missing."); - } + var messageType = ReadInt32(unpacker, "messageType"); switch (messageType) { @@ -90,18 +91,27 @@ private static StreamItemMessage CreateStreamItemMessage(Unpacker unpacker, IInv private static CompletionMessage CreateCompletionMessage(Unpacker unpacker, IInvocationBinder binder) { var invocationId = ReadInvocationId(unpacker); - var error = ReadString(unpacker, "error"); + var resultKind = ReadInt32(unpacker, "resultKind"); - var hasResult = false; + string error = null; object result = null; - if (error == null) + var hasResult = false; + + switch(resultKind) { - hasResult = ReadBoolean(unpacker, "hasResult"); - if (hasResult) - { + case ErrorResult: + error = ReadString(unpacker, "error"); + break; + case NonVoidResult: var itemType = binder.GetReturnType(invocationId); result = DeserializeObject(unpacker, itemType, "argument"); - } + hasResult = true; + break; + case VoidResult: + hasResult = false; + break; + default: + throw new FormatException("Invalid invocation result kind."); } return new CompletionMessage(invocationId, error, result, hasResult); @@ -153,16 +163,22 @@ private void WriteStremingItemMessage(StreamItemMessage streamItemMessage, Packe private void WriteCompletionMessage(CompletionMessage completionMessage, Packer packer, Stream output) { + var resultKind = + completionMessage.Error != null ? ErrorResult : + completionMessage.HasResult ? NonVoidResult : + VoidResult; + packer.Pack(CompletionMessageType); packer.PackString(completionMessage.InvocationId); - packer.PackString(completionMessage.Error); - if (completionMessage.Error == null) + packer.Pack(resultKind); + switch (resultKind) { - packer.Pack(completionMessage.HasResult); - if (completionMessage.HasResult) - { + case ErrorResult: + packer.PackString(completionMessage.Error); + break; + case NonVoidResult: packer.PackObject(completionMessage.Result); - } + break; } } @@ -171,6 +187,24 @@ private static string ReadInvocationId(Unpacker unpacker) return ReadString(unpacker, "invocationId"); } + private static int ReadInt32(Unpacker unpacker, string field) + { + Exception msgPackException = null; + try + { + if (unpacker.ReadInt32(out var value)) + { + return value; + } + } + catch (Exception e) + { + msgPackException = e; + } + + throw new FormatException($"Reading '{field}' as Int32 failed.", msgPackException); + } + private static string ReadString(Unpacker unpacker, string field) { Exception msgPackException = null; diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index fed9cbf6e4..0ce3294aa6 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -73,7 +73,8 @@ public void CanRoundTripInvocationMessage(HubMessage[] hubMessages) public static IEnumerable InvalidPayloads => new[] { - new object[] { new byte[0], "Message type is missing." }, + new object[] { new byte[0], "Reading 'messageType' as Int32 failed." }, + new object[] { new byte[] { 0xc2 } , "Reading 'messageType' as Int32 failed." }, // message type is not int new object[] { new byte[] { 0x0a } , "Invalid message type: 10." }, // InvocationMessage @@ -100,13 +101,15 @@ public void CanRoundTripInvocationMessage(HubMessage[] hubMessages) // CompletionMessage new object[] { new byte[] { 0x03 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false new object[] { new byte[] { 0x03, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'error' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xa1 }, "Reading 'error' as String failed." }, // error is cut - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc0 }, "Reading 'hasResult' as Boolean failed." }, // hasResult missing - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc0, 0xa0 }, "Reading 'hasResult' as Boolean failed." }, // 0xa0 is string - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc0, 0xc3 }, "Deserializing object of the `String` type for 'argument' failed." }, // result missing - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc0, 0xc3, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc0, 0xc3, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." } // return type mismatch + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'resultKind' as Int32 failed." }, // result kind is not int + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x0f }, "Invalid invocation result kind." }, // result kind is out of range + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01 }, "Reading 'error' as String failed." }, // error result but no error + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01, 0xa1 }, "Reading 'error' as String failed." }, // error is cut + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03 }, "Deserializing object of the `String` type for 'argument' failed." }, // non void result but result missing + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut + new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // return type mismatch + + // TODO: ReadAsInt32 and no int32 value }; [Theory] @@ -132,8 +135,8 @@ public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExcept [InlineData(new object[] { new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x03, 0xa1, 0x78, 0xa1, 0x45, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x03, 0xa1, 0x78, 0xa1, 0x45, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xa1, 0x78, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xa1, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x03, 0xa1 }, 2 })] public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount) From d42aefee983ae98cd18408d04f629ff0ebf449ff Mon Sep 17 00:00:00 2001 From: moozzyk Date: Sat, 1 Jul 2017 11:36:59 -0700 Subject: [PATCH 2/3] Turning messages to arrays Makes it much easier to parse in JavaScript (also is more MsgPacky) --- .../Protocol/MessagePackHubProtocol.cs | 4 ++ .../Protocol/MessagePackHubProtocolTests.cs | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index cdfc0f0e16..e8fc729f6e 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -42,6 +42,7 @@ public bool TryParseMessages(ReadOnlyBuffer input, IInvocationBinder binde private static HubMessage ParseMessage(Stream input, IInvocationBinder binder) { var unpacker = Unpacker.Create(input); + _ = ReadArrayLength(unpacker, "elementCount"); var messageType = ReadInt32(unpacker, "messageType"); switch (messageType) @@ -147,6 +148,7 @@ private void WriteMessageCore(HubMessage message, Stream output) private static void WriteInvocationMessage(InvocationMessage invocationMessage, Packer packer, Stream output) { + packer.PackArrayHeader(5); packer.Pack(InvocationMessageType); packer.PackString(invocationMessage.InvocationId); packer.Pack(invocationMessage.NonBlocking); @@ -156,6 +158,7 @@ private static void WriteInvocationMessage(InvocationMessage invocationMessage, private void WriteStremingItemMessage(StreamItemMessage streamItemMessage, Packer packer, Stream output) { + packer.PackArrayHeader(3); packer.Pack(StreamItemMessageType); packer.PackString(streamItemMessage.InvocationId); packer.PackObject(streamItemMessage.Item); @@ -168,6 +171,7 @@ private void WriteCompletionMessage(CompletionMessage completionMessage, Packer completionMessage.HasResult ? NonVoidResult : VoidResult; + packer.PackArrayHeader(2 + resultKind != VoidResult ? 1 : 0); packer.Pack(CompletionMessageType); packer.PackString(completionMessage.InvocationId); packer.Pack(resultKind); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index 0ce3294aa6..97f3dfff89 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -73,41 +73,42 @@ public void CanRoundTripInvocationMessage(HubMessage[] hubMessages) public static IEnumerable InvalidPayloads => new[] { - new object[] { new byte[0], "Reading 'messageType' as Int32 failed." }, - new object[] { new byte[] { 0xc2 } , "Reading 'messageType' as Int32 failed." }, // message type is not int - new object[] { new byte[] { 0x0a } , "Invalid message type: 10." }, + new object[] { new byte[0], "Reading array length for 'elementCount' failed." }, + new object[] { new byte[] { 0x91 }, "Reading 'messageType' as Int32 failed." }, + new object[] { new byte[] { 0x91, 0xc2 } , "Reading 'messageType' as Int32 failed." }, // message type is not int + new object[] { new byte[] { 0x91, 0x0a } , "Invalid message type: 10." }, // InvocationMessage - new object[] { new byte[] { 0x01 }, "Reading 'invocationId' as String failed." }, // invocationId missing - new object[] { new byte[] { 0x01, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking missing - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking is not bool - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'target' as String failed." }, // target missing - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0x00 }, "Reading 'target' as String failed." }, // 0x00 is Int - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1 }, "Reading 'target' as String failed." }, // string is cut - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x00 }, "Reading array length for 'arguments' failed." }, // 0x00 is not array marker - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91 }, "Deserializing object of the `String` type for 'argument' failed." }, // array is missing elements - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91, 0xa2, 0x78 }, "Deserializing object of the `String` type for 'argument' failed." }, // array element is cut - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x92, 0xa0, 0x00 }, "Target method expects 1 arguments(s) but invocation has 2 argument(s)." }, // argument count does not match binder argument count - new object[] { new byte[] { 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // argument type mismatch + new object[] { new byte[] { 0x95, 0x01 }, "Reading 'invocationId' as String failed." }, // invocationId missing + new object[] { new byte[] { 0x95, 0x01, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking missing + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Reading 'nonBlocking' as Boolean failed." }, // nonBlocking is not bool + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'target' as String failed." }, // target missing + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0x00 }, "Reading 'target' as String failed." }, // 0x00 is Int + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1 }, "Reading 'target' as String failed." }, // string is cut + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78 }, "Reading array length for 'arguments' failed." }, // array is missing + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x00 }, "Reading array length for 'arguments' failed." }, // 0x00 is not array marker + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91 }, "Deserializing object of the `String` type for 'argument' failed." }, // array is missing elements + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91, 0xa2, 0x78 }, "Deserializing object of the `String` type for 'argument' failed." }, // array element is cut + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x92, 0xa0, 0x00 }, "Target method expects 1 arguments(s) but invocation has 2 argument(s)." }, // argument count does not match binder argument count + new object[] { new byte[] { 0x95, 0x01, 0xa3, 0x78, 0x79, 0x7a, 0xc2, 0xa1, 0x78, 0x91, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // argument type mismatch // StreamItemMessage - new object[] { new byte[] { 0x02 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x02, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x02, 0xa3, 0x78, 0x79, 0x7a }, "Deserializing object of the `String` type for 'item' failed." }, // item is missing - new object[] { new byte[] { 0x02, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Deserializing object of the `String` type for 'item' failed." }, // item type mismatch + new object[] { new byte[] { 0x93, 0x02 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false + new object[] { new byte[] { 0x93, 0x02, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false + new object[] { new byte[] { 0x93, 0x02, 0xa3, 0x78, 0x79, 0x7a }, "Deserializing object of the `String` type for 'item' failed." }, // item is missing + new object[] { new byte[] { 0x93, 0x02, 0xa3, 0x78, 0x79, 0x7a, 0x00 }, "Deserializing object of the `String` type for 'item' failed." }, // item type mismatch // CompletionMessage - new object[] { new byte[] { 0x03 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x03, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'resultKind' as Int32 failed." }, // result kind is not int - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x0f }, "Invalid invocation result kind." }, // result kind is out of range - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01 }, "Reading 'error' as String failed." }, // error result but no error - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01, 0xa1 }, "Reading 'error' as String failed." }, // error is cut - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03 }, "Deserializing object of the `String` type for 'argument' failed." }, // non void result but result missing - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut - new object[] { new byte[] { 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // return type mismatch + new object[] { new byte[] { 0x93, 0x03 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false + new object[] { new byte[] { 0x93, 0x03, 0xc2 }, "Reading 'invocationId' as String failed." }, // 0xc2 is Bool false + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0xc2 }, "Reading 'resultKind' as Int32 failed." }, // result kind is not int + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x0f }, "Invalid invocation result kind." }, // result kind is out of range + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01 }, "Reading 'error' as String failed." }, // error result but no error + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x01, 0xa1 }, "Reading 'error' as String failed." }, // error is cut + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03 }, "Deserializing object of the `String` type for 'argument' failed." }, // non void result but result missing + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut + new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // return type mismatch // TODO: ReadAsInt32 and no int32 value }; @@ -135,9 +136,9 @@ public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExcept [InlineData(new object[] { new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xa1, 0x78, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0xa1, 0x78, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x03, 0xa1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1 }, 2 })] public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount) { From a3ab684c76294e78c8e556e6b0a1ee437d87381e Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Fri, 30 Jun 2017 17:33:55 -0700 Subject: [PATCH 3/3] Adding MsgPack hub protocol to TS client --- .../Formatters.spec.ts | 42 ++++++- .../MessagePackHubProtocol.spec.ts | 103 +++++++++++++++ .../Formatters.ts | 54 +++++++- .../HubConnection.ts | 4 +- .../IHubProtocol.ts | 4 +- .../MessagePackHubProtocol.ts | 118 ++++++++++++++++++ .../gulpfile.js | 1 + .../wwwroot/js/hubConnectionTests.js | 12 +- client-ts/package.json | 2 + .../Protocol/MessagePackHubProtocol.cs | 6 +- .../Protocol/MessagePackHubProtocolTests.cs | 2 - 11 files changed, 331 insertions(+), 17 deletions(-) create mode 100644 client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts create mode 100644 client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts index 9c7501461f..dbd43c19a1 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts @@ -1,4 +1,4 @@ -import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters" +import { TextMessageFormat, BinaryMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters" describe("Text Message Formatter", () => { it("should return empty array on empty input", () => { @@ -30,3 +30,43 @@ describe("Text Message Formatter", () => { }); }); }); + +describe("Binary Message Formatter", () => { + ([ + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [ new Uint8Array([])]], + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff], [ new Uint8Array([0xff])]], + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f], [ new Uint8Array([0xff]), new Uint8Array([0x7f])]], + ] as [[number[], Uint8Array[]]]).forEach(([payload, expected_messages]) => { + it(`should parse '${payload}' correctly`, () => { + let messages = BinaryMessageFormat.parse(new Uint8Array(payload).buffer); + expect(messages).toEqual(expected_messages); + }) + }); + + ([ + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Cannot read message size")], + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x80, 0x00], new Error("Cannot read message size")], + [[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], + [[0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], + [[0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], + [[0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], + [[0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], + [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], new Error("Incomplete message")], + ] as [[number[], Error]]).forEach(([payload, expected_error]) => { + it(`should fail to parse '${payload}'`, () => { + expect(() => BinaryMessageFormat.parse(new Uint8Array(payload).buffer)).toThrow(expected_error); + }) + }); + + ([ + [[], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]], + [[0x20], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20]], + ] as [[number[], number[]]]).forEach(([input, expected_payload]) => { + it(`should write '${input}'`, () => { + let actual = new Uint8Array(BinaryMessageFormat.write(new Uint8Array(input))); + let expected = new Uint8Array(expected_payload); + expect(actual).toEqual(expected); + }) + }); +}); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts new file mode 100644 index 0000000000..8d62a2c30a --- /dev/null +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts @@ -0,0 +1,103 @@ +import { MessagePackHubProtocol } from "../Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol" +import { MessageType, InvocationMessage, CompletionMessage, ResultMessage } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol" + +describe("MessageHubProtocol", () => { + it("can write/read Invocation message", () => { + let invocation = { + type: MessageType.Invocation, + invocationId: "123", + target: "myMethod", + nonblocking: true, + arguments: [42, true, "test", ["x1", "y2"], null] + }; + + let protocol = new MessagePackHubProtocol(); + var parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation)); + expect(parsedMessages).toEqual([invocation]); + }); + + ([ + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, + 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72], + { + type: MessageType.Completion, + invocationId: "abc", + error: "Err", + result: null + } as CompletionMessage ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, + 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ], + { + type: MessageType.Completion, + invocationId: "abc", + error: null, + result: "OK" + } as CompletionMessage ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ], + { + type: MessageType.Completion, + invocationId: "abc", + error: null, + result: null + } as CompletionMessage ] + ] as [[number[], CompletionMessage]]).forEach(([payload, expected_message]) => + it("can read Completion message", () => { + let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); + expect(messages).toEqual([expected_message]); + })); + + ([ + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ], + { + type: MessageType.Result, + invocationId: "abc", + item: 8 + } as ResultMessage ] + ] as [[number[], CompletionMessage]]).forEach(([payload, expected_message]) => + it("can read Result message", () => { + let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); + expect(messages).toEqual([expected_message]); + })); + + ([ + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], new Error("Invalid payload.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90 ], new Error("Invalid payload.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc2 ], new Error("Invalid payload.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ] + ] as [[number[], Error]]).forEach(([payload, expected_error]) => + it("throws for invalid messages", () => { + expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer)) + .toThrow(expected_error); + })); + + it("can read multiple messages", () => { + let payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, + 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ]; + let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); + expect(messages).toEqual([ + { + type: MessageType.Result, + invocationId: "abc", + item: 8 + } as ResultMessage, + { + type: MessageType.Completion, + invocationId: "abc", + error: null, + result: "OK" + } as CompletionMessage + ]); + }); +}); \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts index 4dd91b5c81..940b09f2da 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts @@ -34,7 +34,7 @@ export namespace TextMessageFormat { if (!hasSpace(input, offset, 1 + length)) { throw new Error("Message is incomplete"); } - + // Read the payload var payload = input.substr(offset, length); offset += length; @@ -66,4 +66,56 @@ export namespace TextMessageFormat { } return messages; } +} + +export namespace BinaryMessageFormat { + export function write(output: Uint8Array): ArrayBuffer { + let size = output.byteLength; + let buffer = new Uint8Array(size + 8); + + // javascript bitwise operators only support 32-bit integers + for (let i = 7; i >= 4; i--) { + buffer[i] = size & 0xff; + size = size >> 8; + } + + buffer.set(output, 8); + + return buffer.buffer; + } + + export function parse(input: ArrayBuffer): Uint8Array[] { + let result: Uint8Array[] = []; + let uint8Array = new Uint8Array(input); + // 8 - the length prefix size + for (let offset = 0; offset < input.byteLength;) { + + if (input.byteLength < offset + 8) { + throw new Error("Cannot read message size") + } + + // Note javascript bitwise operators only support 32-bit integers - for now cutting bigger messages. + // Tracking bug https://github.com/aspnet/SignalR/issues/613 + if (!(uint8Array[offset] == 0 && uint8Array[offset + 1] == 0 && uint8Array[offset + 2] == 0 + && uint8Array[offset + 3] == 0 && (uint8Array[offset + 4] & 0x80) == 0)) { + throw new Error("Messages bigger than 2147483647 bytes are not supported"); + } + + let size = 0; + for (let i = 4; i < 8; i++) { + size = (size << 8) | uint8Array[offset + i]; + } + + if (uint8Array.byteLength >= (offset + 8 + size)) { + result.push(uint8Array.slice(offset + 8, offset + 8 + size)) + } + else { + throw new Error("Incomplete message"); + } + + offset = offset + 8 + size; + } + + return result; + } } \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index 06061cbfff..96164bc9d3 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -16,8 +16,9 @@ export class HubConnection { private connectionClosedCallback: ConnectionClosed; private protocol: IHubProtocol; - constructor(connection: IConnection) { + constructor(connection: IConnection, protocol: IHubProtocol = new JsonHubProtocol()) { this.connection = connection; + this.protocol = protocol || new JsonHubProtocol(); this.connection.onDataReceived = data => { this.onDataReceived(data); }; @@ -28,7 +29,6 @@ export class HubConnection { this.callbacks = new Map void>(); this.methods = new Map void>(); this.id = 0; - this.protocol = new JsonHubProtocol(); } private onDataReceived(data: any) { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts index dfe03ffe11..ff1ff3de6e 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts @@ -30,6 +30,6 @@ export interface NegotiationMessage { export interface IHubProtocol { name(): string; - parseMessages(input: string): HubMessage[]; - writeMessage(message: HubMessage): string; + parseMessages(input: any): HubMessage[]; + writeMessage(message: HubMessage): any; } \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts new file mode 100644 index 0000000000..812f57cda2 --- /dev/null +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts @@ -0,0 +1,118 @@ +import { IHubProtocol, MessageType, HubMessage, InvocationMessage, ResultMessage, CompletionMessage } from "./IHubProtocol"; +import { BinaryMessageFormat } from "./Formatters" +import * as msgpack5 from "msgpack5" + +export class MessagePackHubProtocol implements IHubProtocol { + name(): string { + return "messagepack"; + } + + parseMessages(input: ArrayBuffer): HubMessage[] { + return BinaryMessageFormat.parse(input).map(m => this.parseMessage(m)); + } + + private parseMessage(input: Uint8Array): HubMessage { + if (input.length == 0) { + throw new Error("Invalid payload."); + } + + let msgpack = msgpack5(); + let properties = msgpack.decode(new Buffer(input)); + if (properties.length == 0 || !(properties instanceof Array)) { + throw new Error("Invalid payload."); + } + + let messageType = properties[0] as MessageType; + switch (messageType) { + case MessageType.Invocation: + return this.createInvocationMessage(properties); + case MessageType.Result: + return this.createStreamItemMessage(properties); + case MessageType.Completion: + return this.createCompletionMessage(properties); + default: + throw new Error("Invalid message type."); + } + } + + private createInvocationMessage(properties: any[]): InvocationMessage { + if (properties.length != 5) { + throw new Error("Invalid payload for Invocation message."); + } + + return { + type: MessageType.Invocation, + invocationId: properties[1], + nonblocking: properties[2], + target: properties[3], + arguments: properties[4] + } as InvocationMessage; + } + + private createStreamItemMessage(properties: any[]): ResultMessage { + if (properties.length != 3) { + throw new Error("Invalid payload for stream Result message."); + } + + return { + type: MessageType.Result, + invocationId: properties[1], + item: properties[2] + } as ResultMessage; + } + + private createCompletionMessage(properties: any[]): CompletionMessage { + if (properties.length < 3) { + throw new Error("Invalid payload for Completion message."); + } + + const errorResult = 1; + const voidResult = 2; + const nonVoidResult = 3; + + let resultKind = properties[2]; + + if ((resultKind === voidResult && properties.length != 3) || + (resultKind !== voidResult && properties.length != 4)) { + throw new Error("Invalid payload for Completion message."); + } + + let completionMessage = { + type: MessageType.Completion, + invocationId: properties[1], + error: null as string, + result: null as any + }; + + switch (resultKind) { + case errorResult: + completionMessage.error = properties[3]; + break; + case nonVoidResult: + completionMessage.result = properties[3]; + break; + } + + return completionMessage as ResultMessage; + } + + writeMessage(message: HubMessage): ArrayBuffer { + switch (message.type) { + case MessageType.Invocation: + return this.writeInvocation(message as InvocationMessage); + case MessageType.Result: + case MessageType.Completion: + throw new Error(`Writing messages of type '${message.type}' is not supported.`); + default: + throw new Error("Invalid message type."); + } + } + + private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer { + let msgpack = msgpack5(); + let payload = msgpack.encode([ MessageType.Invocation, invocationMessage.invocationId, + invocationMessage.nonblocking, invocationMessage.target, invocationMessage.arguments]); + + return BinaryMessageFormat.write(payload.slice()); + } +} \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js index a5b47fab36..dccfb511f5 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js @@ -24,6 +24,7 @@ gulp.task('browserify-client', ['compile-ts-client'], () => { .pipe(gulp.dest(clientOutDir + '/../browser')); }); + gulp.task('build-ts-client', ['clean', 'compile-ts-client', 'browserify-client']); gulp.task('default', ['build-ts-client']); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js index 242fdf20be..5f6296d14d 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js @@ -5,7 +5,7 @@ describe('hubConnection', () => { describe(`${signalR.TransportType[transportType]} transport`, () => { it(`can invoke server method and receive result`, done => { const message = "Hi"; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), 'formatType=json&format=text'); + let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); hubConnection.onClosed = error => { expect(error).toBe(undefined); done(); @@ -31,7 +31,7 @@ describe('hubConnection', () => { }); it(`can stream server method and receive result`, done => { - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), 'formatType=json&format=text'); + let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); hubConnection.onClosed = error => { expect(error).toBe(undefined); done(); @@ -63,7 +63,7 @@ describe('hubConnection', () => { it(`rethrows an exception from the server when invoking`, done => { const errorMessage = "An error occurred."; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), 'formatType=json&format=text'); + let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); hubConnection.start() .then(() => { @@ -90,7 +90,7 @@ describe('hubConnection', () => { it(`rethrows an exception from the server when streaming`, done => { const errorMessage = "An error occurred."; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), 'formatType=json&format=text'); + let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); hubConnection.start() .then(() => { @@ -116,7 +116,7 @@ describe('hubConnection', () => { }); it(`can receive server calls`, done => { - let client = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), 'formatType=json&format=text'); + let client = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); const message = "Hello SignalR"; let callbackPromise = new Promise((resolve, reject) => { @@ -150,7 +150,7 @@ describe('hubConnection', () => { ServerSentEvents: "Error occurred" }; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(`http://${document.location.host}/uncreatable`, { transport: transportType }), 'formatType=json&format=text'); + let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(`http://${document.location.host}/uncreatable`, { transport: transportType })); hubConnection.onClosed = error => { expect(error).toMatch(errorRegex[signalR.TransportType[transportType]]); diff --git a/client-ts/package.json b/client-ts/package.json index 03d269eccd..8909a32e62 100644 --- a/client-ts/package.json +++ b/client-ts/package.json @@ -23,11 +23,13 @@ }, "homepage": "https://github.com/aspnet/SignalR#readme", "devDependencies": { + "@types/msgpack5": "^3.4.0", "browserify": "^13.1.1", "del": "^2.2.2", "gulp": "^3.9.1", "gulp-typescript": "^3.1.3", "jasmine": "^2.5.2", + "msgpack5": "^3.5.0", "typescript": "^2.0.10", "vinyl-source-stream": "^1.1.0" } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index e8fc729f6e..9b62789983 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -136,7 +136,7 @@ private void WriteMessageCore(HubMessage message, Stream output) WriteInvocationMessage(invocationMessage, packer, output); break; case StreamItemMessage streamItemMessage: - WriteStremingItemMessage(streamItemMessage, packer, output); + WriteStreamingItemMessage(streamItemMessage, packer, output); break; case CompletionMessage completionMessage: WriteCompletionMessage(completionMessage, packer, output); @@ -156,7 +156,7 @@ private static void WriteInvocationMessage(InvocationMessage invocationMessage, packer.PackObject(invocationMessage.Arguments); } - private void WriteStremingItemMessage(StreamItemMessage streamItemMessage, Packer packer, Stream output) + private void WriteStreamingItemMessage(StreamItemMessage streamItemMessage, Packer packer, Stream output) { packer.PackArrayHeader(3); packer.Pack(StreamItemMessageType); @@ -171,7 +171,7 @@ private void WriteCompletionMessage(CompletionMessage completionMessage, Packer completionMessage.HasResult ? NonVoidResult : VoidResult; - packer.PackArrayHeader(2 + resultKind != VoidResult ? 1 : 0); + packer.PackArrayHeader(3 + resultKind != VoidResult ? 1 : 0); packer.Pack(CompletionMessageType); packer.PackString(completionMessage.InvocationId); packer.Pack(resultKind); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index 97f3dfff89..eeccf7f75d 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -109,8 +109,6 @@ public void CanRoundTripInvocationMessage(HubMessage[] hubMessages) new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03 }, "Deserializing object of the `String` type for 'argument' failed." }, // non void result but result missing new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0xa9 }, "Deserializing object of the `String` type for 'argument' failed." }, // result is cut new object[] { new byte[] { 0x93, 0x03, 0xa3, 0x78, 0x79, 0x7a, 0x03, 0x00 }, "Deserializing object of the `String` type for 'argument' failed." }, // return type mismatch - - // TODO: ReadAsInt32 and no int32 value }; [Theory]