diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
index 884967d73d78..8c3fd43461fb 100644
--- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
+++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
@@ -69,6 +69,7 @@
+
diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
index cf77ba81c063..29990edece34 100644
--- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
+++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
@@ -1,4 +1,4 @@
-
+Core components of ASP.NET Core Kestrel cross-platform web server.
@@ -29,6 +29,7 @@
+
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index a6aba0228722..51f5199f387d 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
diff --git a/src/Servers/Kestrel/Core/src/Internal/CancellationTokenSourcePool.cs b/src/Shared/CancellationTokenSourcePool.cs
similarity index 96%
rename from src/Servers/Kestrel/Core/src/Internal/CancellationTokenSourcePool.cs
rename to src/Shared/CancellationTokenSourcePool.cs
index 72e6465b7851..94279b1f970f 100644
--- a/src/Servers/Kestrel/Core/src/Internal/CancellationTokenSourcePool.cs
+++ b/src/Shared/CancellationTokenSourcePool.cs
@@ -3,7 +3,7 @@
using System.Collections.Concurrent;
-namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+namespace Microsoft.AspNetCore.Internal;
internal sealed class CancellationTokenSourcePool
{
diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs
index 1d831c9d28c9..67eb2878ade3 100644
--- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs
+++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs
@@ -232,7 +232,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<
{
try
{
- var result = await hubContext.Clients.Client(id).InvokeAsync("Result");
+ var result = await hubContext.Clients.Client(id).InvokeAsync("Result", cancellationToken: default);
return result.ToString(CultureInfo.InvariantCulture);
}
catch (Exception ex)
diff --git a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
index 34a5f17dd19d..38694e211d3f 100644
--- a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
+++ b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
index c3930f66dda2..6d726e0c2c17 100644
--- a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
+++ b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
@@ -230,8 +230,16 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message)
else
{
// If we have an invocation id already we can parse the end result
- var returnType = binder.GetReturnType(invocationId);
- result = BindType(ref reader, input, returnType);
+ var returnType = ProtocolHelper.TryGetReturnType(binder, invocationId);
+ if (returnType is null)
+ {
+ reader.Skip();
+ result = null;
+ }
+ else
+ {
+ result = BindType(ref reader, input, returnType);
+ }
}
}
else if (reader.ValueTextEquals(ItemPropertyNameBytes.EncodedUtf8Bytes))
@@ -408,8 +416,15 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message)
if (hasResultToken)
{
- var returnType = binder.GetReturnType(invocationId);
- result = BindType(ref resultToken, input, returnType);
+ var returnType = ProtocolHelper.TryGetReturnType(binder, invocationId);
+ if (returnType is null)
+ {
+ result = null;
+ }
+ else
+ {
+ result = BindType(ref resultToken, input, returnType);
+ }
}
message = BindCompletionMessage(invocationId, error, result, hasResult);
diff --git a/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj b/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj
index 69554b03f11a..7d3882c020b3 100644
--- a/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj
+++ b/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocolWorker.cs b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocolWorker.cs
index 8ce10b662bd9..df0dc0c7a8a9 100644
--- a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocolWorker.cs
+++ b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocolWorker.cs
@@ -162,14 +162,21 @@ private CompletionMessage CreateCompletionMessage(ref MessagePackReader reader,
error = ReadString(ref reader, "error");
break;
case NonVoidResult:
- var itemType = binder.GetReturnType(invocationId);
- if (itemType == typeof(RawResult))
+ var itemType = ProtocolHelper.TryGetReturnType(binder, invocationId);
+ if (itemType is null)
{
- result = new RawResult(reader.ReadRaw());
+ reader.Skip();
}
else
{
- result = DeserializeObject(ref reader, itemType, "argument");
+ if (itemType == typeof(RawResult))
+ {
+ result = new RawResult(reader.ReadRaw());
+ }
+ else
+ {
+ result = DeserializeObject(ref reader, itemType, "argument");
+ }
}
hasResult = true;
break;
diff --git a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj
index 2167aa456d47..f905437355d7 100644
--- a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj
+++ b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/SignalR/common/Protocols.NewtonsoftJson/src/Protocol/NewtonsoftJsonHubProtocol.cs b/src/SignalR/common/Protocols.NewtonsoftJson/src/Protocol/NewtonsoftJsonHubProtocol.cs
index 2df8002c66d9..11dd9d107adb 100644
--- a/src/SignalR/common/Protocols.NewtonsoftJson/src/Protocol/NewtonsoftJsonHubProtocol.cs
+++ b/src/SignalR/common/Protocols.NewtonsoftJson/src/Protocol/NewtonsoftJsonHubProtocol.cs
@@ -209,21 +209,28 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message)
else
{
// If we have an invocation id already we can parse the end result
- var returnType = binder.GetReturnType(invocationId);
-
- if (!JsonUtils.ReadForType(reader, returnType))
- {
- throw new JsonReaderException("Unexpected end when reading JSON");
- }
-
- if (returnType == typeof(RawResult))
+ var returnType = ProtocolHelper.TryGetReturnType(binder, invocationId);
+ if (returnType is null)
{
- var token = JToken.Load(reader);
- result = GetRawResult(token);
+ reader.Skip();
+ result = null;
}
else
{
- result = PayloadSerializer.Deserialize(reader, returnType);
+ if (!JsonUtils.ReadForType(reader, returnType))
+ {
+ throw new JsonReaderException("Unexpected end when reading JSON");
+ }
+
+ if (returnType == typeof(RawResult))
+ {
+ var token = JToken.Load(reader);
+ result = GetRawResult(token);
+ }
+ else
+ {
+ result = PayloadSerializer.Deserialize(reader, returnType);
+ }
}
}
break;
@@ -397,14 +404,21 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message)
if (resultToken != null)
{
- var returnType = binder.GetReturnType(invocationId);
- if (returnType == typeof(RawResult))
+ var returnType = ProtocolHelper.TryGetReturnType(binder, invocationId);
+ if (returnType is null)
{
- result = GetRawResult(resultToken);
+ result = null;
}
else
{
- result = resultToken.ToObject(returnType, PayloadSerializer);
+ if (returnType == typeof(RawResult))
+ {
+ result = GetRawResult(resultToken);
+ }
+ else
+ {
+ result = resultToken.ToObject(returnType, PayloadSerializer);
+ }
}
}
diff --git a/src/SignalR/common/Shared/ClientResultsManager.cs b/src/SignalR/common/Shared/ClientResultsManager.cs
index 97b5d2c7023d..12544fb649dd 100644
--- a/src/SignalR/common/Shared/ClientResultsManager.cs
+++ b/src/SignalR/common/Shared/ClientResultsManager.cs
@@ -144,7 +144,7 @@ public void RegisterCancellation()
{
// TODO: RedisHubLifetimeManager will want to notify the other server (if there is one) about the cancellation
// so it can clean up state and potentially forward that info to the connection
- _clientResultsManager.TryCompleteResult(_connectionId, CompletionMessage.WithError(_invocationId, "Canceled"));
+ _clientResultsManager.TryCompleteResult(_connectionId, CompletionMessage.WithError(_invocationId, "Invocation canceled by the server."));
}
public new void SetResult(T result)
diff --git a/src/SignalR/common/Shared/TryGetReturnType.cs b/src/SignalR/common/Shared/TryGetReturnType.cs
new file mode 100644
index 000000000000..1cfcd0d189f1
--- /dev/null
+++ b/src/SignalR/common/Shared/TryGetReturnType.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.AspNetCore.SignalR.Protocol;
+
+internal static class ProtocolHelper
+{
+ internal static Type? TryGetReturnType(IInvocationBinder binder, string invocationId)
+ {
+ try
+ {
+ return binder.GetReturnType(invocationId);
+ }
+ // GetReturnType throws if invocationId not found, this can be caused by the server canceling a client-result but the client still sending a result
+ // For now let's ignore the failure and skip parsing the result, server will log that the result wasn't expected anymore and ignore the message
+ // In the future we may want a CompletionBindingFailureMessage that we can flow to the dispatcher for handling
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
index dcc68f102b14..59e765acf0c4 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
@@ -1,16 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.Buffers;
-using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
-using Xunit;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol;
@@ -460,6 +455,19 @@ public void RawResultRoundTripsProperly(string testDataName)
}
}
+ [Fact]
+ public void UnexpectedClientResultGivesEmptyCompletionMessage()
+ {
+ var binder = new TestBinder();
+ var message = Frame("{\"type\":3,\"result\":1,\"invocationId\":\"1\"}");
+ var data = new ReadOnlySequence(Encoding.UTF8.GetBytes(message));
+ Assert.True(JsonHubProtocol.TryParseMessage(ref data, binder, out var hubMessage));
+
+ var completion = Assert.IsType(hubMessage);
+ Assert.Null(completion.Result);
+ Assert.Equal("1", completion.InvocationId);
+ }
+
public static string Frame(string input)
{
var data = Encoding.UTF8.GetBytes(input);
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs
index 978d729cc4d5..647f50cd3324 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs
@@ -1,15 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.Buffers;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
-using Xunit;
namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol;
@@ -249,6 +243,19 @@ public void RawResultRoundTripsProperly(string testDataName)
}
}
+ [Fact]
+ public void UnexpectedClientResultGivesEmptyCompletionMessage()
+ {
+ var binder = new TestBinder();
+ var input = Frame(Convert.FromBase64String("lQOAo3h5egPA"));
+ var data = new ReadOnlySequence(input);
+ Assert.True(HubProtocol.TryParseMessage(ref data, binder, out var hubMessage));
+
+ var completion = Assert.IsType(hubMessage);
+ Assert.Null(completion.Result);
+ Assert.Equal("xyz", completion.InvocationId);
+ }
+
public class ClientResultTestData
{
public string Name { get; }
diff --git a/src/SignalR/server/Core/src/ClientProxyExtensions.cs b/src/SignalR/server/Core/src/ClientProxyExtensions.cs
index 7b2f0c8c8b8f..c7716ef8af51 100644
--- a/src/SignalR/server/Core/src/ClientProxyExtensions.cs
+++ b/src/SignalR/server/Core/src/ClientProxyExtensions.cs
@@ -227,7 +227,7 @@ public static Task SendAsync(this IClientProxy clientProxy, string method, objec
/// The token to monitor for cancellation requests. The default value is .
/// A that represents the asynchronous invoke.
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")]
- public static Task InvokeAsync(this ISingleClientProxy clientProxy, string method, CancellationToken cancellationToken = default)
+ public static Task InvokeAsync(this ISingleClientProxy clientProxy, string method, CancellationToken cancellationToken)
{
return clientProxy.InvokeCoreAsync(method, Array.Empty