diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs
index b8d479a555..b5801e2ee3 100644
--- a/src/Components/Server/src/Circuits/CircuitHost.cs
+++ b/src/Components/Server/src/Circuits/CircuitHost.cs
@@ -108,20 +108,31 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
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 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);
}
-
- await OnCircuitOpenedAsync(cancellationToken);
-
- await OnConnectionUpAsync(cancellationToken);
});
-
- _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 1603ac602d..76fb91374d 100644
--- a/src/Components/Server/src/ComponentHub.cs
+++ b/src/Components/Server/src/ComponentHub.cs
@@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Server
///
/// 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 @@ namespace Microsoft.AspNetCore.Components.Server
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 3b907e8592..559399d9e9 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 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
handler2.VerifyAll();
}
+ [Fact]
+ public async Task InitializeAsync_ReportsOwnAsyncExceptions()
+ {
+ // Arrange
+ var handler = new Mock(MockBehavior.Strict);
+ var tcs = new TaskCompletionSource