Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hang on deserialization exception #388

Merged
merged 2 commits into from
Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/StreamJsonRpc.Tests/JsonRpcJsonHeadersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,28 @@ public async Task CanPassExceptionFromServer()

protected override void InitializeFormattersAndHandlers()
{
this.clientMessageFormatter = new JsonMessageFormatter { JsonSerializer = { Converters = { new UnserializableTypeConverter() } } };
this.serverMessageFormatter = new JsonMessageFormatter { JsonSerializer = { Converters = { new UnserializableTypeConverter() } } };
this.clientMessageFormatter = new JsonMessageFormatter
{
JsonSerializer =
{
Converters =
{
new UnserializableTypeConverter(),
new TypeThrowsWhenDeserializedConverter(),
},
},
};
this.serverMessageFormatter = new JsonMessageFormatter
{
JsonSerializer =
{
Converters =
{
new UnserializableTypeConverter(),
new TypeThrowsWhenDeserializedConverter(),
},
},
};

this.serverMessageHandler = new HeaderDelimitedMessageHandler(this.serverStream, this.serverStream, this.serverMessageFormatter);
this.clientMessageHandler = new HeaderDelimitedMessageHandler(this.clientStream, this.clientStream, this.clientMessageFormatter);
Expand Down Expand Up @@ -221,6 +241,20 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
}
}

private class TypeThrowsWhenDeserializedConverter : JsonConverter<TypeThrowsWhenDeserialized>
{
public override TypeThrowsWhenDeserialized ReadJson(JsonReader reader, Type objectType, TypeThrowsWhenDeserialized existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new Exception("This exception is meant to be thrown.");
}

public override void WriteJson(JsonWriter writer, TypeThrowsWhenDeserialized value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WriteEndObject();
}
}

private class StringBase64Converter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(string);
Expand Down
15 changes: 14 additions & 1 deletion src/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected override void InitializeFormattersAndHandlers()

var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
new IMessagePackFormatter[] { new UnserializableTypeFormatter() },
new IMessagePackFormatter[] { new UnserializableTypeFormatter(), new TypeThrowsWhenDeserializedFormatter() },
new IFormatterResolver[] { StandardResolverAllowPrivate.Instance }));
((MessagePackFormatter)this.serverMessageFormatter).SetMessagePackSerializerOptions(options);
((MessagePackFormatter)this.clientMessageFormatter).SetMessagePackSerializerOptions(options);
Expand All @@ -83,4 +83,17 @@ public void Serialize(ref MessagePackWriter writer, CustomSerializedType value,
writer.Write(value?.Value);
}
}

private class TypeThrowsWhenDeserializedFormatter : IMessagePackFormatter<TypeThrowsWhenDeserialized>
{
public TypeThrowsWhenDeserialized Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
throw new Exception("This exception is meant to be thrown.");
}

public void Serialize(ref MessagePackWriter writer, TypeThrowsWhenDeserialized value, MessagePackSerializerOptions options)
{
writer.WriteArrayHeader(0);
}
}
}
12 changes: 12 additions & 0 deletions src/StreamJsonRpc.Tests/JsonRpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,12 @@ public async Task FormatterFatalException()
await Assert.ThrowsAsync<ConnectionLostException>(() => this.clientRpc.InvokeAsync<string>(nameof(Server.AsyncMethod), "Fail"));
}

[Fact]
public async Task ReturnTypeThrowsOnDeserialization()
{
await Assert.ThrowsAnyAsync<Exception>(() => this.clientRpc.InvokeWithCancellationAsync<TypeThrowsWhenDeserialized>(nameof(Server.GetTypeThrowsWhenDeserialized), cancellationToken: this.TimeoutToken)).WithCancellation(this.TimeoutToken);
}

protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down Expand Up @@ -1861,6 +1867,8 @@ public int InstanceMethodWithSingleObjectParameterAndCancellationToken(XAndYFiel
return fields.x + fields.y;
}

public TypeThrowsWhenDeserialized GetTypeThrowsWhenDeserialized() => new TypeThrowsWhenDeserialized();

public int? MethodReturnsNullableInt(int a) => a > 0 ? (int?)a : null;

public int MethodAcceptsNullableArgs(int? a, int? b) => (a.HasValue ? 1 : 0) + (b.HasValue ? 1 : 0);
Expand Down Expand Up @@ -2165,6 +2173,10 @@ public class CustomSerializedType
public string? Value { get; set; }
}

public class TypeThrowsWhenDeserialized
{
}

[DataContract]
public class XAndYFields
{
Expand Down
6 changes: 5 additions & 1 deletion src/StreamJsonRpc/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2163,6 +2163,7 @@ private async Task ReadAndHandleRequestsAsync()
private async Task HandleRpcAsync(JsonRpcMessage rpc)
{
Requires.NotNull(rpc, nameof(rpc));
OutstandingCallData? data = null;
try
{
if (rpc is JsonRpcRequest request)
Expand Down Expand Up @@ -2221,7 +2222,6 @@ private async Task HandleRpcAsync(JsonRpcMessage rpc)
JsonRpcResult? result = resultOrError as JsonRpcResult;
JsonRpcError? error = resultOrError as JsonRpcError;

OutstandingCallData? data = null;
lock (this.dispatcherMapLock)
{
if (this.resultDispatcherMap.TryGetValue(resultOrError.RequestId, out data))
Expand Down Expand Up @@ -2260,6 +2260,7 @@ private async Task HandleRpcAsync(JsonRpcMessage rpc)
// Complete the caller's request with the response asynchronously so it doesn't delay handling of other JsonRpc messages.
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
data.CompletionHandler(rpc);
data = null; // avoid invoking again if we throw later
}
}
else
Expand All @@ -2279,6 +2280,9 @@ private async Task HandleRpcAsync(JsonRpcMessage rpc)

// Fatal error. Raise disconnected event.
this.OnJsonRpcDisconnected(eventArgs);

// If we extracted this callback from the collection already, take care to complete it to avoid hanging our client.
data?.CompletionHandler(null);
}
}

Expand Down
26 changes: 18 additions & 8 deletions src/StreamJsonRpc/MessagePackFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace StreamJsonRpc
using System.IO;
using System.IO.Pipelines;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using MessagePack;
using MessagePack.Formatters;
Expand Down Expand Up @@ -1330,12 +1331,6 @@ public override bool TryGetArgumentByNameOrIndex(string? name, int position, Typ
value = MessagePackSerializer.Deserialize(typeHint ?? typeof(object), ref reader, this.formatter.userDataSerializationOptions);
return true;
}
catch (NotSupportedException)
{
// This block can be removed after https://github.com/neuecc/MessagePack-CSharp/pull/633 is applied.
value = null;
return false;
}
catch (MessagePackSerializationException)
{
value = null;
Expand All @@ -1354,6 +1349,8 @@ private class JsonRpcResult : Protocol.JsonRpcResult, IJsonRpcMessageBufferManag
{
private readonly MessagePackSerializerOptions serializerOptions;

private Exception? resultDeserializationException;

internal JsonRpcResult(MessagePackSerializerOptions serializerOptions)
{
this.serializerOptions = serializerOptions ?? throw new ArgumentNullException(nameof(serializerOptions));
Expand All @@ -1372,6 +1369,11 @@ void IJsonRpcMessageBufferManager.DeserializationComplete(JsonRpcMessage message

public override T GetResult<T>()
{
if (this.resultDeserializationException != null)
{
ExceptionDispatchInfo.Capture(this.resultDeserializationException).Throw();
}

return this.MsgPackResult.IsEmpty
? (T)this.Result!
: MessagePackSerializer.Deserialize<T>(this.MsgPackResult, this.serializerOptions);
Expand All @@ -1382,8 +1384,16 @@ protected internal override void SetExpectedResultType(Type resultType)
Verify.Operation(!this.MsgPackResult.IsEmpty, "Result is no longer available or has already been deserialized.");

var reader = new MessagePackReader(this.MsgPackResult);
this.Result = MessagePackSerializer.Deserialize(resultType, ref reader, this.serializerOptions);
this.MsgPackResult = default;
try
{
this.Result = MessagePackSerializer.Deserialize(resultType, ref reader, this.serializerOptions);
this.MsgPackResult = default;
}
catch (MessagePackSerializationException ex)
{
// This was a best effort anyway. We'll throw again later at a more convenient time for JsonRpc.
this.resultDeserializationException = ex;
}
}
}

Expand Down