Skip to content

Commit c918d72

Browse files
authored
[Blazor] [Fixes #11847] Renderer.DispatchEventAsync throws null reference exception if event handler throws synchronously (#12393)
[Blazor] [Fixes #11847] Renderer.DispatchEventAsync throws null reference exception if event handler throws synchronously * Returns after handling the exception. * Adds a unit test and an E2E test to validate expected behavior.
1 parent d846cb4 commit c918d72

File tree

4 files changed

+63
-11
lines changed

4 files changed

+63
-11
lines changed

src/Components/Components/src/Rendering/Renderer.cs

+1
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fiel
235235
catch (Exception e)
236236
{
237237
HandleException(e);
238+
return Task.CompletedTask;
238239
}
239240
finally
240241
{

src/Components/Components/test/RendererTest.cs

+35-11
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ public void DispatchingEventsWithoutAsyncWorkShouldCompleteSynchronously()
456456
}
457457

458458
[Fact]
459-
public async Task CanDispatchEventsToTopLevelComponents()
459+
public void CanDispatchEventsToTopLevelComponents()
460460
{
461461
// Arrange: Render a component with an event handler
462462
var renderer = new TestRenderer();
@@ -482,12 +482,42 @@ public async Task CanDispatchEventsToTopLevelComponents()
482482
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
483483
Assert.True(renderTask.IsCompletedSuccessfully);
484484
Assert.Same(eventArgs, receivedArgs);
485+
}
486+
487+
[Fact]
488+
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
489+
{
490+
// Arrange: Render a component with an event handler
491+
var renderer = new TestRenderer {
492+
ShouldHandleExceptions = true
493+
};
494+
495+
var component = new EventComponent
496+
{
497+
OnTest = args => throw new Exception("Error")
498+
};
499+
var componentId = renderer.AssignRootComponentId(component);
500+
component.TriggerRender();
501+
502+
var eventHandlerId = renderer.Batches.Single()
503+
.ReferenceFrames
504+
.First(frame => frame.AttributeValue != null)
505+
.AttributeEventHandlerId;
506+
507+
// Assert: Event not yet fired
508+
Assert.Empty(renderer.HandledExceptions);
509+
510+
// Act/Assert: Event can be fired
511+
var eventArgs = new UIEventArgs();
512+
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
513+
Assert.True(renderTask.IsCompletedSuccessfully);
485514

486-
await renderTask; // Does not throw
515+
var exception = Assert.Single(renderer.HandledExceptions);
516+
Assert.Equal("Error", exception.Message);
487517
}
488518

489519
[Fact]
490-
public async Task CanDispatchTypedEventsToTopLevelComponents()
520+
public void CanDispatchTypedEventsToTopLevelComponents()
491521
{
492522
// Arrange: Render a component with an event handler
493523
var renderer = new TestRenderer();
@@ -513,12 +543,10 @@ public async Task CanDispatchTypedEventsToTopLevelComponents()
513543
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
514544
Assert.True(renderTask.IsCompletedSuccessfully);
515545
Assert.Same(eventArgs, receivedArgs);
516-
517-
await renderTask; // does not throw
518546
}
519547

520548
[Fact]
521-
public async Task CanDispatchActionEventsToTopLevelComponents()
549+
public void CanDispatchActionEventsToTopLevelComponents()
522550
{
523551
// Arrange: Render a component with an event handler
524552
var renderer = new TestRenderer();
@@ -544,12 +572,10 @@ public async Task CanDispatchActionEventsToTopLevelComponents()
544572
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
545573
Assert.True(renderTask.IsCompletedSuccessfully);
546574
Assert.NotNull(receivedArgs);
547-
548-
await renderTask; // does not throw
549575
}
550576

551577
[Fact]
552-
public async Task CanDispatchEventsToNestedComponents()
578+
public void CanDispatchEventsToNestedComponents()
553579
{
554580
UIEventArgs receivedArgs = null;
555581

@@ -586,8 +612,6 @@ public async Task CanDispatchEventsToNestedComponents()
586612
var renderTask = renderer.DispatchEventAsync(eventHandlerId, eventArgs);
587613
Assert.True(renderTask.IsCompletedSuccessfully);
588614
Assert.Same(eventArgs, receivedArgs);
589-
590-
await renderTask; // does not throw
591615
}
592616

593617
[Fact]

src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs

+23
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,29 @@ await Assert.ThrowsAsync<TaskCanceledException>(() => Client.InvokeDotNetMethod(
490490
await ValidateClientKeepsWorking(Client, batches);
491491
}
492492

493+
494+
[Fact]
495+
public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit()
496+
{
497+
// Arrange
498+
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
499+
await GoToTestComponent(batches);
500+
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
501+
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
502+
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
503+
504+
// Act
505+
await Client.ClickAsync("event-handler-throw-sync");
506+
507+
Assert.Contains(
508+
logEvents,
509+
e => LogLevel.Warning == e.logLevel &&
510+
"UnhandledExceptionInCircuit" == e.eventIdName &&
511+
"Handler threw an exception" == e.exception.Message);
512+
513+
await ValidateClientKeepsWorking(Client, batches);
514+
}
515+
493516
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) =>
494517
ValidateClientKeepsWorking(Client, () => batches.Count);
495518

src/Components/test/testassets/BasicTestApp/ServerReliability/ReliabilityComponent.razor

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
<button id="triggerjsinterop-success" @onclick="@TriggerJSInteropSuccess">Trigger successfull JS interop callback</button>
2020
<button id="triggerjsinterop-failure" @onclick="@TriggerJSInteropFailure">Trigger error JS interop callback</button>
2121

22+
<button id="event-handler-throw-sync" @onclick="@TriggerSyncException">Trigger sync exception</button>
23+
2224
<button id="thecounter" @onclick="@IncrementCount">Click me</button>
2325

2426
@code
@@ -33,6 +35,8 @@
3335
currentCount++;
3436
}
3537

38+
void TriggerSyncException() => throw new Exception("Handler threw an exception");
39+
3640
async Task TriggerJSInterop()
3741
{
3842
try

0 commit comments

Comments
 (0)