Resolve the JSInterop call if deserialization fails (dotnet/extensions#1802)

* Resolve the JSInterop call if deserialization fails

\n\nCommit migrated from 26cf0c9f4e
This commit is contained in:
Pranav K 2019-06-13 21:54:05 -07:00 committed by GitHub
parent 2dd8eb61ba
commit 867453a06d
7 changed files with 79 additions and 6 deletions

View File

@ -0,0 +1,10 @@
{
"solution": {
"path": "..\\..\\Extensions.sln",
"projects": [
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
"src\\JSInterop\\Microsoft.JSInterop\\test\\Microsoft.JSInterop.Tests.csproj",
"src\\JSInterop\\Mono.WebAssembly.Interop\\src\\Mono.WebAssembly.Interop.csproj",
]
}
}

View File

@ -37,6 +37,7 @@ namespace Microsoft.JSInterop
public partial class JSException : System.Exception public partial class JSException : System.Exception
{ {
public JSException(string message) { } public JSException(string message) { }
public JSException(string message, System.Exception innerException) { }
} }
public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime
{ {

View File

@ -227,11 +227,10 @@ namespace Microsoft.JSInterop
catch catch
{ {
// Always dispose the JsonDocument in case of an error. // Always dispose the JsonDocument in case of an error.
jsonDocument.Dispose(); jsonDocument?.Dispose();
throw; throw;
} }
static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType) static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType)
{ {
// Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's // Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's

View File

@ -17,5 +17,14 @@ namespace Microsoft.JSInterop
public JSException(string message) : base(message) public JSException(string message) : base(message)
{ {
} }
/// <summary>
/// Constructs an instance of <see cref="JSException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public JSException(string message, Exception innerException) : base(message, innerException)
{
}
} }
} }

View File

@ -94,10 +94,18 @@ namespace Microsoft.JSInterop
if (succeeded) if (succeeded)
{ {
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
var result = asyncCallResult != null ? try
JsonSerializer.Parse(asyncCallResult.JsonElement.GetRawText(), resultType, JsonSerializerOptionsProvider.Options) : {
null; var result = asyncCallResult != null ?
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); JsonSerializer.Parse(asyncCallResult.JsonElement.GetRawText(), resultType, JsonSerializerOptionsProvider.Options) :
null;
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
}
catch (Exception exception)
{
var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details.";
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception));
}
} }
else else
{ {

View File

@ -363,6 +363,24 @@ namespace Microsoft.JSInterop.Tests
Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception);
}); });
[Fact]
public Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() => WithJSRuntime(async jsRuntime =>
{
// Arrange
var callId = "123";
var resultTask = jsRuntime.NextInvocationTask;
DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, "InvocableStaticWithParams", default, "<xml>not json</xml>");
await resultTask; // This won't throw, it sets properties on the jsRuntime.
// Assert
using var jsonDocument = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson);
var result = jsonDocument.RootElement;
Assert.Equal(callId, result[0].GetString());
Assert.False(result[1].GetBoolean()); // Fails
Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result[2].GetString());
});
Task WithJSRuntime(Action<TestJSRuntime> testCode) Task WithJSRuntime(Action<TestJSRuntime> testCode)
{ {
return WithJSRuntime(jsRuntime => return WithJSRuntime(jsRuntime =>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.JSInterop.Internal; using Microsoft.JSInterop.Internal;
using Xunit; using Xunit;
@ -86,6 +87,33 @@ namespace Microsoft.JSInterop.Tests
Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message); Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message);
} }
[Fact]
public async Task CanCompleteAsyncCallsWithErrorsDuringDeserialization()
{
// Arrange
var runtime = new TestJSRuntime();
// Act/Assert: Tasks not initially completed
var unrelatedTask = runtime.InvokeAsync<string>("unrelated call", Array.Empty<object>());
var task = runtime.InvokeAsync<int>("test identifier", Array.Empty<object>());
Assert.False(unrelatedTask.IsCompleted);
Assert.False(task.IsCompleted);
using var jsonDocument = JsonDocument.Parse("\"Not a string\"");
// Act/Assert: Task can be failed
runtime.OnEndInvoke(
runtime.BeginInvokeCalls[1].AsyncHandle,
/* succeeded: */ true,
new JSAsyncCallResult(jsonDocument, jsonDocument.RootElement));
Assert.False(unrelatedTask.IsCompleted);
var jsException = await Assert.ThrowsAsync<JSException>(() => task);
Assert.IsType<JsonException>(jsException.InnerException);
// Verify we've disposed the JsonDocument.
Assert.Throws<ObjectDisposedException>(() => jsonDocument.RootElement.Type);
}
[Fact] [Fact]
public void CannotCompleteSameAsyncCallMoreThanOnce() public void CannotCompleteSameAsyncCallMoreThanOnce()
{ {