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(); + var reportedErrors = new List(); + + handler + .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), It.IsAny())) + .Returns(tcs.Task) + .Verifiable(); + + var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object }); + circuitHost.UnhandledException += (sender, errorInfo) => + { + Assert.Same(circuitHost, sender); + reportedErrors.Add(errorInfo); + }; + + // Act + var initializeAsyncTask = circuitHost.InitializeAsync(new CancellationToken()); + + // Assert: No synchronous exceptions + handler.VerifyAll(); + Assert.Empty(reportedErrors); + + // Act: Trigger async exception + var ex = new InvalidTimeZoneException(); + tcs.SetException(ex); + + // Assert: The top-level task still succeeds, because the intended usage + // pattern is fire-and-forget. + await initializeAsyncTask; + + // Assert: The async exception was reported via the side-channel + Assert.Same(ex, reportedErrors.Single().ExceptionObject); + Assert.False(reportedErrors.Single().IsTerminating); + } + [Fact] public async Task DisposeAsync_InvokesCircuitHandler() {