diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs
index b8d479a555d9..b5801e2ee3d0 100644
--- a/src/Components/Server/src/Circuits/CircuitHost.cs
+++ b/src/Components/Server/src/Circuits/CircuitHost.cs
@@ -108,20 +108,31 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
{
await Renderer.InvokeAsync(async () =>
{
- SetCurrentCircuitHost(this);
-
- for (var i = 0; i < Descriptors.Count; i++)
+ try
{
- var (componentType, domElementSelector) = Descriptors[i];
- await Renderer.AddComponentAsync(componentType, domElementSelector);
- }
+ SetCurrentCircuitHost(this);
+ _initialized = true; // We're ready to accept incoming JSInterop calls from here on
- await OnCircuitOpenedAsync(cancellationToken);
+ await OnCircuitOpenedAsync(cancellationToken);
+ await OnConnectionUpAsync(cancellationToken);
- await OnConnectionUpAsync(cancellationToken);
+ // We add the root components *after* the circuit is flagged as open.
+ // That's because AddComponentAsync waits for quiescence, which can take
+ // arbitrarily long. In the meantime we might need to be receiving and
+ // processing incoming JSInterop calls or similar.
+ for (var i = 0; i < Descriptors.Count; i++)
+ {
+ var (componentType, domElementSelector) = Descriptors[i];
+ await Renderer.AddComponentAsync(componentType, domElementSelector);
+ }
+ }
+ catch (Exception ex)
+ {
+ // We have to handle all our own errors here, because the upstream caller
+ // has to fire-and-forget this
+ Renderer_UnhandledException(this, ex);
+ }
});
-
- _initialized = true;
}
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs
index 1603ac602d7e..76fb91374def 100644
--- a/src/Components/Server/src/ComponentHub.cs
+++ b/src/Components/Server/src/ComponentHub.cs
@@ -64,7 +64,7 @@ public override Task OnDisconnectedAsync(Exception exception)
///
/// Intended for framework use only. Applications should not call this method directly.
///
- public async Task StartCircuit(string uriAbsolute, string baseUriAbsolute)
+ public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
{
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
@@ -76,8 +76,11 @@ public async Task StartCircuit(string uriAbsolute, string baseUriAbsolut
circuitHost.UnhandledException += CircuitHost_UnhandledException;
- // If initialization fails, this will throw. The caller will fail if they try to call into any interop API.
- await circuitHost.InitializeAsync(Context.ConnectionAborted);
+ // Fire-and-forget the initialization process, because we can't block the
+ // SignalR message loop (we'd get a deadlock if any of the initialization
+ // logic relied on receiving a subsequent message from SignalR), and it will
+ // take care of its own errors anyway.
+ _ = circuitHost.InitializeAsync(Context.ConnectionAborted);
_circuitRegistry.Register(circuitHost);
diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs
index 3b907e8592fb..559399d9e90e 100644
--- a/src/Components/Server/test/Circuits/CircuitHostTest.cs
+++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
@@ -81,6 +82,46 @@ public async Task InitializeAsync_InvokesHandlers()
handler2.VerifyAll();
}
+ [Fact]
+ public async Task InitializeAsync_ReportsOwnAsyncExceptions()
+ {
+ // Arrange
+ var handler = new Mock(MockBehavior.Strict);
+ var tcs = new TaskCompletionSource