The `NegotiationResponse` object is incorrectly deserialized with the `NegotiateProtocol.ParseResponse` API
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Describe the bug
The `NegotiationResponse` object is incorrectly deserialized with the `NegotiateProtocol.ParseResponse` API. The default behavior for the `NegotiationResponse` type assumes its serialized in a specific way, where properties are ignored if they're `null`, this is not the default behavior, however; of the `System.Text.Json` serializer for both `JsonSerializerDefaults.Web` or `JsonSeriaizerDefaults.Default`. For example, when a response is serialized as follows:
```csharp
var response = new NegotiationResponse
{
Url = "https://example.com/signalr",
AccessToken = "fake-access-token"
};
var json = JsonSerializer.Serialize(
response,
new JsonSerializerOptions(JsonSerializerDefaults.Web));
```
The resulting JSON is as follows:
```json
{"url":"https://example.com/signalr","accessToken":"fake-access-token","connectionId":null,"availableTransports":null}
```
When calling `NegotiateProtocol.ParseResponse` from this JSON, theere's an exception thrown that is both misleading and inaccurate. It results in the following exception:
```
System.IO.InvalidDataException : Invalid negotiation response received.
---- System.IO.InvalidDataException : Expected 'connectionId' to be of type String.
```
While the `connectionId` is in fact `null`, there is a `url`—which should be the only thing required when deserializing the response, per:
https://github.com/dotnet/aspnetcore/blob/f3555640d3b0d049856947c4f2bd0b869adf5c5e/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs#L233
Additionally, I've confirmed that this bug doesn't exist in the SignalR JavaScript client. See https://github.com/dotnet/aspire/blob/main/playground/signalr/SignalRServerlessWeb/Pages/Index.cshtml.
### Expected Behavior
I would expect that the `NegotiateProtocol.ParseResponse` would either successfully deserialize the `NegotiationResponse` object, even if there are `null` properties.
Ideally, client code shouldn't have to know to explicit ignore `null` properties when deserializing this type. This seems related to https://github.com/Azure/azure-functions-dotnet-worker/issues/2213.
### Steps To Reproduce
Here's a sample xUnit project the demonstrates two tests, one that fails and one that passes, I'd expect both of these tests to pass:
**_Project.csproj_**:
```xml
net9.0
enable
enable
false
```
**_UnitTest1.cs_**:
```csharp
using Microsoft.AspNetCore.Http.Connections;
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit.Abstractions;
namespace NegotiateResponse.Tests;
public class UnitTest1(ITestOutputHelper output)
{
private static readonly JsonSerializerOptions s_webJsonOptions = new(JsonSerializerDefaults.Web);
private static readonly JsonSerializerOptions s_webJsonOptionsWithIgnoreNull = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Fact]
public void RoundTripFailsWithMisleadingAndInaccurateError()
{
var newResponse = new NegotiationResponse
{
Url = "https://example.com/signalr",
AccessToken = "fake-access-token"
};
var json = JsonSerializer.Serialize(newResponse, s_webJsonOptions);
output.WriteLine($"Response JSON: {json}");
using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
/* This is a bug, the NegotiateProtocol.ParseResponse method should
not throw an exception when given valid JSON.
{"url":"https://example.com/signalr","accessToken":"fake-access-token","connectionId":null,"availableTransports":null}
The NegotiateProtocol.ParseResponse method should be able to
parse the JSON response correctly, but it throws an exception ->
System.IO.InvalidDataException : Invalid negotiation response received.
---- System.IO.InvalidDataException : Expected 'connectionId' to be of type String.
The connectionId is only actually needed when the `url` is `null`, but the `url` isn't `null`.
Instead, the ParseResponse method incorrectly parses the JSON and throws an exception.
*/
NegotiationResponse parsedResponse = NegotiateProtocol.ParseResponse(memoryStream);
Assert.NotNull(parsedResponse);
Assert.Equal(newResponse.Url, parsedResponse.Url);
Assert.Equal(newResponse.AccessToken, parsedResponse.AccessToken);
}
[Fact]
public void RoundTripSucceedsWithExplicitAndUnknownSerializationRules()
{
var newResponse = new NegotiationResponse
{
Url = "https://example.com/signalr",
AccessToken = "fake-access-token"
};
var json = JsonSerializer.Serialize(newResponse, s_webJsonOptionsWithIgnoreNull);
output.WriteLine($"Response JSON: {json}");
using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
var parsedResponse = NegotiateProtocol.ParseResponse(memoryStream);
Assert.NotNull(parsedResponse);
Assert.Equal(newResponse.Url, parsedResponse.Url);
Assert.Equal(newResponse.AccessToken, parsedResponse.AccessToken);
}
}
```
### Exceptions (if any)
```
System.IO.InvalidDataException : Invalid negotiation response received.
---- System.IO.InvalidDataException : Expected 'connectionId' to be of type String.
```
### .NET Version
9.0.200
### Anything else?
```
.NET SDK:
Version: 9.0.200
Commit: 90e8b202f2
Workload version: 9.0.200-manifests.34e45f8a
MSBuild version: 17.13.8+cbc39bea8
Runtime Environment:
OS Name: Windows
OS Version: 10.0.26100
OS Platform: Windows
RID: win-x64
Base Path: C:\Program Files\dotnet\sdk\9.0.200\
.NET workloads installed:
[aspire]
Installation Source: VS 17.13.35806.99, VS 17.14.35806.103
Manifest Version: 8.2.2/8.0.100
Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.aspire\8.2.2\WorkloadManifest.json
Install Type: Msi
Configured to use loose manifests when installing new manifests.
Host:
Version: 9.0.3
Architecture: x64
Commit: 831d23e561
.NET SDKs installed:
8.0.310 [C:\Program Files\dotnet\sdk]
9.0.104 [C:\Program Files\dotnet\sdk]
9.0.200 [C:\Program Files\dotnet\sdk]
.NET runtimes installed:
Microsoft.AspNetCore.App 8.0.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 9.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 9.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 8.0.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 9.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 9.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 8.0.13 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 9.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 9.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Other architectures found:
x86 [C:\Program Files (x86)\dotnet]
registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]
Environment variables:
Not set
global.json file:
Not found
Learn more:
https://aka.ms/dotnet/info
Download .NET:
https://aka.ms/dotnet/download
```
Contributor guide
Assessment
This issue has not been assessed yet.