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 JSException(string message) { }
public JSException(string message, System.Exception innerException) { }
}
public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime
{

View File

@ -227,11 +227,10 @@ namespace Microsoft.JSInterop
catch
{
// Always dispose the JsonDocument in case of an error.
jsonDocument.Dispose();
jsonDocument?.Dispose();
throw;
}
static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType)
{
// 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)
{
}
/// <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)
{
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
var result = asyncCallResult != null ?
JsonSerializer.Parse(asyncCallResult.JsonElement.GetRawText(), resultType, JsonSerializerOptionsProvider.Options) :
null;
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
try
{
var result = asyncCallResult != null ?
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
{

View File

@ -363,6 +363,24 @@ namespace Microsoft.JSInterop.Tests
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)
{
return WithJSRuntime(jsRuntime =>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.JSInterop.Internal;
using Xunit;
@ -86,6 +87,33 @@ namespace Microsoft.JSInterop.Tests
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]
public void CannotCompleteSameAsyncCallMoreThanOnce()
{