Skip to content

Commit e8c99b2

Browse files
committed
[Blazor][Fixes #12054] ComponentHub reliability improvements.
* Validates StartCircuit is called once per circuit. * NOOPs when other hub methods are called before start circuit and returns an error to the client.
1 parent fb64c8f commit e8c99b2

File tree

3 files changed

+234
-22
lines changed

3 files changed

+234
-22
lines changed

src/Components/Server/src/ComponentHub.cs

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Runtime.CompilerServices;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Components.Server.Circuits;
78
using Microsoft.AspNetCore.Http;
@@ -73,6 +74,13 @@ public override Task OnDisconnectedAsync(Exception exception)
7374
/// </summary>
7475
public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
7576
{
77+
if (CircuitHost != null)
78+
{
79+
Log.CircuitAlreadyInitialized(_logger, CircuitHost.CircuitId);
80+
NotifyClientError(Clients.Caller, $"The circuit host '{CircuitHost.CircuitId}' has already been initialized.");
81+
return null;
82+
}
83+
7684
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
7785
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
7886
{
@@ -129,26 +137,60 @@ public async Task<bool> ConnectCircuit(string circuitId)
129137
/// </summary>
130138
public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
131139
{
132-
_ = EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
140+
if (CircuitHost == null)
141+
{
142+
Log.CircuitHostNotInitialized(_logger);
143+
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
144+
return;
145+
}
146+
147+
_ = CircuitHost.BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
133148
}
134149

150+
/// <summary>
151+
/// Intended for framework use only. Applications should not call this method directly.
152+
/// </summary>
135153
public void EndInvokeJSFromDotNet(long asyncHandle, bool succeeded, string arguments)
136154
{
137-
_ = EnsureCircuitHost().EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
155+
if (CircuitHost == null)
156+
{
157+
Log.CircuitHostNotInitialized(_logger);
158+
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
159+
return;
160+
}
161+
162+
_ = CircuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
138163
}
139164

165+
/// <summary>
166+
/// Intended for framework use only. Applications should not call this method directly.
167+
/// </summary>
140168
public void DispatchBrowserEvent(string eventDescriptor, string eventArgs)
141169
{
142-
_ = EnsureCircuitHost().DispatchEvent(eventDescriptor, eventArgs);
170+
if (CircuitHost == null)
171+
{
172+
Log.CircuitHostNotInitialized(_logger);
173+
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
174+
return;
175+
}
176+
177+
_ = CircuitHost.DispatchEvent(eventDescriptor, eventArgs);
143178
}
144179

145180
/// <summary>
146181
/// Intended for framework use only. Applications should not call this method directly.
147182
/// </summary>
148183
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
149184
{
185+
if (CircuitHost == null)
186+
{
187+
Log.CircuitHostNotInitialized(_logger);
188+
NotifyClientError(Clients.Caller, "Circuit not initialized.");
189+
return;
190+
}
191+
150192
Log.ReceivedConfirmationForBatch(_logger, renderId);
151-
EnsureCircuitHost().Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
193+
CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
152194
}
153195

154196
private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
@@ -161,14 +203,14 @@ private async void CircuitHost_UnhandledException(object sender, UnhandledExcept
161203
Log.UnhandledExceptionInCircuit(_logger, circuitId, (Exception)e.ExceptionObject);
162204
if (_options.DetailedErrors)
163205
{
164-
await circuitHost.Client.SendAsync("JS.Error", e.ExceptionObject.ToString());
206+
await NotifyClientError(circuitHost.Client, e.ExceptionObject.ToString());
165207
}
166208
else
167209
{
168210
var message = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
169211
$"detailed exceptions in '{typeof(CircuitOptions).Name}.{nameof(CircuitOptions.DetailedErrors)}'";
170212

171-
await circuitHost.Client.SendAsync("JS.Error", message);
213+
await NotifyClientError(circuitHost.Client, message);
172214
}
173215

174216
// We generally can't abort the connection here since this is an async
@@ -181,17 +223,8 @@ private async void CircuitHost_UnhandledException(object sender, UnhandledExcept
181223
}
182224
}
183225

184-
private CircuitHost EnsureCircuitHost()
185-
{
186-
var circuitHost = CircuitHost;
187-
if (circuitHost == null)
188-
{
189-
var message = $"The {nameof(CircuitHost)} is null. This is due to an exception thrown during initialization.";
190-
throw new InvalidOperationException(message);
191-
}
192-
193-
return circuitHost;
194-
}
226+
private static Task NotifyClientError(IClientProxy client, string error) =>
227+
client.SendAsync("JS.Error", error);
195228

196229
private static class Log
197230
{
@@ -207,6 +240,13 @@ private static class Log
207240
private static readonly Action<ILogger, string, Exception> _failedToTransmitException =
208241
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, "FailedToTransmitException"), "Failed to transmit exception to client in circuit {CircuitId}");
209242

243+
private static readonly Action<ILogger, string, Exception> _circuitAlreadyInitialized =
244+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "CircuitAlreadyInitialized"), "The circuit host '{CircuitId}' has already been initialized");
245+
246+
private static readonly Action<ILogger, string, Exception> _circuitHostNotInitialized =
247+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "CircuitHostNotInitialized"), "Call to '{CallSite}' received before the circuit host initialization.");
248+
249+
210250
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName)
211251
{
212252
_noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
@@ -226,6 +266,10 @@ public static void FailedToTransmitException(ILogger logger, string circuitId, E
226266
{
227267
_failedToTransmitException(logger, circuitId, transmissionException);
228268
}
269+
270+
public static void CircuitAlreadyInitialized(ILogger logger, string circuitId) => _circuitAlreadyInitialized(logger, circuitId, null);
271+
272+
public static void CircuitHostNotInitialized(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostNotInitialized(logger, callSite, null);
229273
}
230274
}
231275
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
using Ignitor;
9+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
10+
using Microsoft.AspNetCore.Components.Rendering;
11+
using Microsoft.AspNetCore.SignalR.Client;
12+
using Xunit;
13+
14+
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
15+
{
16+
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>
17+
{
18+
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
19+
private readonly AspNetSiteServerFixture _serverFixture;
20+
21+
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture)
22+
{
23+
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
24+
_serverFixture = serverFixture;
25+
CreateDefaultConfiguration();
26+
}
27+
28+
public BlazorClient Client { get; set; }
29+
30+
private IList<Batch> Batches { get; set; } = new List<Batch>();
31+
private IList<string> Errors { get; set; } = new List<string>();
32+
33+
private void CreateDefaultConfiguration()
34+
{
35+
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
36+
Client.RenderBatchReceived += (id, rendererId, data) => Batches.Add(new Batch(id, rendererId, data));
37+
Client.OnCircuitError += (error) => Errors.Add(error);
38+
}
39+
40+
[Fact]
41+
public async Task CannotStartMultipleCircuits()
42+
{
43+
// Arrange
44+
var expectedError = "The circuit host '.*?' has already been initialized.";
45+
var rootUri = _serverFixture.RootUri;
46+
var baseUri = new Uri(rootUri, "/subdir");
47+
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
48+
Assert.Single(Batches);
49+
50+
// Act
51+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
52+
"StartCircuit",
53+
baseUri.GetLeftPart(UriPartial.Authority),
54+
baseUri));
55+
56+
// Assert
57+
var actualError = Assert.Single(Errors);
58+
Assert.Matches(expectedError, actualError);
59+
}
60+
61+
[Fact]
62+
public async Task CannotInvokeJSInteropBeforeInitialization()
63+
{
64+
// Arrange
65+
var expectedError = "Circuit not initialized.";
66+
var rootUri = _serverFixture.RootUri;
67+
var baseUri = new Uri(rootUri, "/subdir");
68+
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
69+
Assert.Empty(Batches);
70+
71+
// Act
72+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
73+
"BeginInvokeDotNetFromJS",
74+
"",
75+
"",
76+
"",
77+
0,
78+
""));
79+
80+
// Assert
81+
var actualError = Assert.Single(Errors);
82+
Assert.Equal(expectedError, actualError);
83+
}
84+
85+
[Fact]
86+
public async Task CannotInvokeOnRenderCompletedInitialization()
87+
{
88+
// Arrange
89+
var expectedError = "Circuit not initialized.";
90+
var rootUri = _serverFixture.RootUri;
91+
var baseUri = new Uri(rootUri, "/subdir");
92+
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
93+
Assert.Empty(Batches);
94+
95+
// Act
96+
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
97+
"OnRenderCompleted",
98+
5,
99+
null));
100+
101+
// Assert
102+
var actualError = Assert.Single(Errors);
103+
Assert.Equal(expectedError, actualError);
104+
}
105+
106+
private class Batch
107+
{
108+
public Batch(int id, int rendererId, byte [] data)
109+
{
110+
Id = id;
111+
RendererId = rendererId;
112+
Data = data;
113+
}
114+
115+
public int Id { get; }
116+
public int RendererId { get; }
117+
public byte[] Data { get; }
118+
}
119+
}
120+
}

src/Components/test/testassets/Ignitor/BlazorClient.cs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public BlazorClient()
4040

4141
private CancellableOperation NextBatchReceived { get; set; }
4242

43+
private CancellableOperation NextErrorReceived { get; set; }
44+
4345
private CancellableOperation NextJSInteropReceived { get; set; }
4446

4547
private CancellableOperation NextDotNetInteropCompletionReceived { get; set; }
@@ -52,7 +54,7 @@ public BlazorClient()
5254

5355
public event Action<string> DotNetInteropCompletion;
5456

55-
public event Action<Error> OnCircuitError;
57+
public event Action<string> OnCircuitError;
5658

5759
public string CircuitId { get; set; }
5860

@@ -98,6 +100,18 @@ public Task PrepareForNextDotNetInterop()
98100
return NextDotNetInteropCompletionReceived.Completion.Task;
99101
}
100102

103+
public Task PrepareForNextCircuitError()
104+
{
105+
if (NextErrorReceived?.Completion != null)
106+
{
107+
throw new InvalidOperationException("Invalid state previous task not completed");
108+
}
109+
110+
NextErrorReceived = new CancellableOperation(DefaultLatencyTimeout);
111+
112+
return NextErrorReceived.Completion.Task;
113+
}
114+
101115
public async Task ClickAsync(string elementId)
102116
{
103117
if (!Hive.TryFindElementById(elementId, out var elementNode))
@@ -139,6 +153,13 @@ public async Task ExpectDotNetInterop(Func<Task> action)
139153
await task;
140154
}
141155

156+
public async Task ExpectCircuitError(Func<Task> action)
157+
{
158+
var task = WaitForCircuitError();
159+
await action();
160+
await task;
161+
}
162+
142163
private Task WaitForRenderBatch(TimeSpan? timeout = null)
143164
{
144165
if (ImplicitWait)
@@ -180,7 +201,20 @@ private async Task WaitForDotNetInterop()
180201
}
181202
}
182203

183-
public async Task<bool> ConnectAsync(Uri uri, bool prerendered)
204+
private async Task WaitForCircuitError()
205+
{
206+
if (ImplicitWait)
207+
{
208+
if (DefaultLatencyTimeout == null)
209+
{
210+
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
211+
}
212+
213+
await PrepareForNextCircuitError();
214+
}
215+
}
216+
217+
public async Task<bool> ConnectAsync(Uri uri, bool prerendered, bool connectAutomatically = true)
184218
{
185219
var builder = new HubConnectionBuilder();
186220
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
@@ -193,9 +227,14 @@ public async Task<bool> ConnectAsync(Uri uri, bool prerendered)
193227
HubConnection.On<int, string, string>("JS.BeginInvokeJS", OnBeginInvokeJS);
194228
HubConnection.On<string>("JS.EndInvokeDotNet", OnEndInvokeDotNet);
195229
HubConnection.On<int, int, byte[]>("JS.RenderBatch", OnRenderBatch);
196-
HubConnection.On<Error>("JS.OnError", OnError);
230+
HubConnection.On<string>("JS.Error", OnError);
197231
HubConnection.Closed += OnClosedAsync;
198232

233+
if (!connectAutomatically)
234+
{
235+
return true;
236+
}
237+
199238
// Now everything is registered so we can start the circuit.
200239
if (prerendered)
201240
{
@@ -264,9 +303,18 @@ private void OnRenderBatch(int browserRendererId, int batchId, byte[] batchData)
264303
}
265304
}
266305

267-
private void OnError(Error error)
306+
private void OnError(string error)
268307
{
269-
OnCircuitError?.Invoke(error);
308+
try
309+
{
310+
OnCircuitError?.Invoke(error);
311+
312+
NextErrorReceived?.Completion?.TrySetResult(null);
313+
}
314+
catch (Exception e)
315+
{
316+
NextErrorReceived?.Completion?.TrySetResult(e);
317+
}
270318
}
271319

272320
private Task OnClosedAsync(Exception ex)

0 commit comments

Comments
 (0)