Skip to content

Commit fa36878

Browse files
authored
Stop enforcing JSON read till end if EndInvokeJS returns early (#39060)
* mono_crash.*.blob * Stop enforcing read till end if EndInvoke returns early * Tests * Test name change * Fix tests * PR Feedback @pranavkm
1 parent 28ecab6 commit fa36878

File tree

4 files changed

+136
-66
lines changed

4 files changed

+136
-66
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ UpgradeLog.htm
4343
.idea
4444
*.svclog
4545
mono_crash.*.json
46+
mono_crash.*.blob

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ public static void EndInvokeJS(JSRuntime jsRuntime, string arguments)
306306
var success = reader.GetBoolean();
307307

308308
reader.Read();
309-
jsRuntime.EndInvokeJS(taskId, success, ref reader);
309+
if (!jsRuntime.EndInvokeJS(taskId, success, ref reader))
310+
{
311+
return;
312+
}
310313

311314
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
312315
{

src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,13 @@ protected internal virtual Task<Stream> ReadJSDataAsStreamAsync(IJSStreamReferen
225225
}
226226

227227
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:RequiresUnreferencedCode", Justification = "We enforce trimmer attributes for JSON deserialized types on InvokeAsync.")]
228-
internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader)
228+
internal bool EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader)
229229
{
230230
if (!_pendingTasks.TryRemove(taskId, out var tcs))
231231
{
232232
// We should simply return if we can't find an id for the invocation.
233233
// This likely means that the method that initiated the call defined a timeout and stopped waiting.
234-
return;
234+
return false;
235235
}
236236

237237
CleanupTasksAndRegistrations(taskId);
@@ -251,11 +251,14 @@ internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe
251251
var exceptionText = jsonReader.GetString() ?? string.Empty;
252252
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText));
253253
}
254+
255+
return true;
254256
}
255257
catch (Exception exception)
256258
{
257259
var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details.";
258260
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception));
261+
return false;
259262
}
260263
}
261264

src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs

+126-63
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ public void CannotUseDotNetObjectRefAfterReleaseDotNetObject()
263263
}
264264

265265
[Fact]
266-
public void EndInvoke_WithSuccessValue()
266+
public void EndInvokeJS_WithSuccessValue()
267267
{
268268
// Arrange
269269
var jsRuntime = new TestJSRuntime();
@@ -282,7 +282,7 @@ public void EndInvoke_WithSuccessValue()
282282
}
283283

284284
[Fact]
285-
public async Task EndInvoke_WithErrorString()
285+
public async Task EndInvokeJS_WithErrorString()
286286
{
287287
// Arrange
288288
var jsRuntime = new TestJSRuntime();
@@ -299,7 +299,7 @@ public async Task EndInvoke_WithErrorString()
299299
}
300300

301301
[Fact]
302-
public async Task EndInvoke_WithNullError()
302+
public async Task EndInvokeJS_WithNullError()
303303
{
304304
// Arrange
305305
var jsRuntime = new TestJSRuntime();
@@ -314,6 +314,129 @@ public async Task EndInvoke_WithNullError()
314314
Assert.Empty(ex.Message);
315315
}
316316

317+
[Fact]
318+
public void EndInvokeJS_DoesNotThrowJSONExceptionIfTaskCancelled()
319+
{
320+
// Arrange
321+
var jsRuntime = new TestJSRuntime();
322+
var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
323+
var cts = new CancellationTokenSource();
324+
var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions);
325+
326+
// Act
327+
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant", cts.Token);
328+
329+
cts.Cancel();
330+
331+
DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
332+
333+
// Assert
334+
Assert.False(task.IsCompletedSuccessfully);
335+
Assert.True(task.IsCanceled);
336+
}
337+
338+
[Fact]
339+
public void EndInvokeJS_ThrowsIfJsonIsEmptyString()
340+
{
341+
// Arrange
342+
var jsRuntime = new TestJSRuntime();
343+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
344+
345+
// Act & Assert
346+
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(jsRuntime, ""));
347+
}
348+
349+
[Fact]
350+
public void EndInvokeJS_ThrowsIfJsonIsNotArray()
351+
{
352+
// Arrange
353+
var jsRuntime = new TestJSRuntime();
354+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
355+
356+
// Act & Assert
357+
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(jsRuntime, $"{{\"key\": \"{jsRuntime.LastInvocationAsyncHandle}\"}}"));
358+
}
359+
360+
[Fact]
361+
public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete()
362+
{
363+
// Arrange
364+
var jsRuntime = new TestJSRuntime();
365+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
366+
367+
// Act & Assert
368+
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, false"));
369+
}
370+
371+
[Fact]
372+
public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments()
373+
{
374+
// Arrange
375+
var jsRuntime = new TestJSRuntime();
376+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
377+
378+
// Act & Assert
379+
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, false, \"Hello\", 5]"));
380+
}
381+
382+
[Fact]
383+
public void EndInvokeJS_DoesNotThrowJSONExceptionIfTaskCancelled_WithMoreThan3Arguments()
384+
{
385+
// Arrange
386+
var jsRuntime = new TestJSRuntime();
387+
var cts = new CancellationTokenSource();
388+
389+
// Act
390+
var task = jsRuntime.InvokeAsync<TestDTO>("unimportant", cts.Token);
391+
392+
cts.Cancel();
393+
394+
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, false, \"Hello\", 5]");
395+
396+
// Assert
397+
Assert.False(task.IsCompletedSuccessfully);
398+
Assert.True(task.IsCanceled);
399+
}
400+
401+
[Fact]
402+
public void EndInvokeJS_Works()
403+
{
404+
// Arrange
405+
var jsRuntime = new TestJSRuntime();
406+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
407+
408+
// Act
409+
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
410+
411+
// Assert
412+
Assert.True(task.IsCompletedSuccessfully);
413+
Assert.Equal(7, task.Result.IntVal);
414+
}
415+
416+
[Fact]
417+
public void EndInvokeJS_WithArrayValue()
418+
{
419+
var jsRuntime = new TestJSRuntime();
420+
var task = jsRuntime.InvokeAsync<int[]>("somemethod");
421+
422+
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
423+
424+
Assert.True(task.IsCompletedSuccessfully);
425+
Assert.Equal(new[] { 1, 2, 3 }, task.Result);
426+
}
427+
428+
[Fact]
429+
public void EndInvokeJS_WithNullValue()
430+
{
431+
var jsRuntime = new TestJSRuntime();
432+
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
433+
434+
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
435+
436+
Assert.True(task.IsCompletedSuccessfully);
437+
Assert.Null(task.Result);
438+
}
439+
317440
[Fact]
318441
public void CanInvokeInstanceMethodWithParams()
319442
{
@@ -652,66 +775,6 @@ public void ParseArguments_Throws_WithIncorrectDotNetObjectRefUsage()
652775
Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
653776
}
654777

655-
[Fact]
656-
public void EndInvokeJS_ThrowsIfJsonIsEmptyString()
657-
{
658-
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), ""));
659-
}
660-
661-
[Fact]
662-
public void EndInvokeJS_ThrowsIfJsonIsNotArray()
663-
{
664-
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}"));
665-
}
666-
667-
[Fact]
668-
public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete()
669-
{
670-
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false"));
671-
}
672-
673-
[Fact]
674-
public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments()
675-
{
676-
Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]"));
677-
}
678-
679-
[Fact]
680-
public void EndInvokeJS_Works()
681-
{
682-
var jsRuntime = new TestJSRuntime();
683-
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
684-
685-
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
686-
687-
Assert.True(task.IsCompletedSuccessfully);
688-
Assert.Equal(7, task.Result.IntVal);
689-
}
690-
691-
[Fact]
692-
public void EndInvokeJS_WithArrayValue()
693-
{
694-
var jsRuntime = new TestJSRuntime();
695-
var task = jsRuntime.InvokeAsync<int[]>("somemethod");
696-
697-
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
698-
699-
Assert.True(task.IsCompletedSuccessfully);
700-
Assert.Equal(new[] { 1, 2, 3 }, task.Result);
701-
}
702-
703-
[Fact]
704-
public void EndInvokeJS_WithNullValue()
705-
{
706-
var jsRuntime = new TestJSRuntime();
707-
var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
708-
709-
DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
710-
711-
Assert.True(task.IsCompletedSuccessfully);
712-
Assert.Null(task.Result);
713-
}
714-
715778
[Fact]
716779
public void ReceiveByteArray_Works()
717780
{

0 commit comments

Comments
 (0)