Skip to content

Commit ed864ce

Browse files
authored
Fix Blazor Server Reconnection Mechanism (#36126)
* Fix Blazor Server Reconnection Mechanism * Fix documentation * Update yarn.lock * Update release.js
1 parent 4736d1c commit ed864ce

File tree

7 files changed

+56
-28
lines changed

7 files changed

+56
-28
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
2929
const jsInitializer = await fetchAndInvokeInitializers(options);
3030

3131
const logger = new ConsoleLogger(options.logLevel);
32-
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
33-
34-
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
35-
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
36-
37-
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
38-
const appState = discoverPersistedState(document);
39-
const circuit = new CircuitDescriptor(components, appState || '');
4032

41-
const initialConnection = await initializeConnection(options, logger, circuit);
42-
const circuitStarted = await circuit.startCircuit(initialConnection);
43-
if (!circuitStarted) {
44-
logger.log(LogLevel.Error, 'Failed to start the circuit.');
45-
return;
46-
}
47-
48-
const reconnect = async (existingConnection?: HubConnection): Promise<boolean> => {
33+
Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => {
4934
if (renderingFailed) {
5035
// We can't reconnect after a failure, so exit early.
5136
return false;
@@ -61,6 +46,21 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
6146

6247
return true;
6348
};
49+
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
50+
51+
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
52+
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
53+
54+
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
55+
const appState = discoverPersistedState(document);
56+
const circuit = new CircuitDescriptor(components, appState || '');
57+
58+
const initialConnection = await initializeConnection(options, logger, circuit);
59+
const circuitStarted = await circuit.startCircuit(initialConnection);
60+
if (!circuitStarted) {
61+
logger.log(LogLevel.Error, 'Failed to start the circuit.');
62+
return;
63+
}
6464

6565
let disconnectSent = false;
6666
const cleanup = () => {
@@ -76,8 +76,6 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
7676

7777
window.addEventListener('unload', cleanup, { capture: false, once: true });
7878

79-
Blazor.reconnect = reconnect;
80-
8179
logger.log(LogLevel.Information, 'Blazor server-side application started.');
8280

8381
jsInitializer.invokeAfterStartedCallbacks(Blazor);
@@ -138,10 +136,15 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
138136

139137
try {
140138
await connection.start();
141-
} catch (ex) {
139+
} catch (ex: any) {
142140
unhandledError(connection, ex as Error, logger);
143141

144-
if (!isNestedError(ex)) {
142+
if (ex.errorType === 'FailedToNegotiateWithServerError') {
143+
// Connection with the server has been interrupted, and we're in the process of reconnecting.
144+
// Throw this exception so it can be handled at the reconnection layer, and don't show the
145+
// error notification.
146+
throw ex;
147+
} else if (!isNestedError(ex)) {
145148
showErrorNotification();
146149
} else if (ex.innerErrors && ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) {
147150
showErrorNotification('Unable to connect, please ensure you are using an updated browser that supports WebSockets.');

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
5353
// - true to mean success
5454
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
5555
// - exception to mean we didn't reach the server (this can be sync or async)
56-
const successful = await (Blazor?.reconnect as any)();
56+
const successful = await Blazor.reconnect!();
5757
if (!successful) {
5858
this.rejected();
5959
}
60-
} catch (err) {
60+
} catch (err: any) {
6161
// We got an exception, server is currently unavailable
6262
this.logger.log(LogLevel.Error, err as Error);
6363
this.failed();

src/Components/Web.JS/src/Platform/Circuits/DefaultReconnectionHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class DefaultReconnectionHandler implements ReconnectionHandler {
1414
constructor(logger: Logger, overrideDisplay?: ReconnectDisplay, reconnectCallback?: () => Promise<boolean>) {
1515
this._logger = logger;
1616
this._reconnectionDisplay = overrideDisplay;
17-
this._reconnectCallback = reconnectCallback || (() => (Blazor.reconnect as any)());
17+
this._reconnectCallback = reconnectCallback || Blazor.reconnect!;
1818
}
1919

2020
onConnectionDown (options: ReconnectionOptions, error?: Error) {
@@ -80,7 +80,7 @@ class ReconnectionProcess {
8080
return;
8181
}
8282
return;
83-
} catch (err) {
83+
} catch (err: any) {
8484
// We got an exception so will try again momentarily
8585
this.logger.log(LogLevel.Error, err as Error);
8686
}

src/Components/Web.JS/yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4547,7 +4547,7 @@ uri-js@^4.2.2:
45474547
dependencies:
45484548
punycode "^2.1.0"
45494549

4550-
url-parse@^1.4.3, url-parse@^1.5.0:
4550+
url-parse@>=1.5.0, url-parse@^1.4.3:
45514551
version "1.5.3"
45524552
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
45534553
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==

src/SignalR/clients/ts/signalr/src/Errors.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,31 @@ export class FailedToStartTransportError extends Error {
158158
}
159159
}
160160

161+
/** Error thrown when the negotiation with the server failed to complete. */
162+
/** @private */
163+
export class FailedToNegotiateWithServerError extends Error {
164+
// @ts-ignore: Intentionally unused.
165+
// eslint-disable-next-line @typescript-eslint/naming-convention
166+
private __proto__: Error;
167+
168+
/** The type name of this error. */
169+
public errorType: string;
170+
171+
/** Constructs a new instance of {@link @microsoft/signalr.FailedToNegotiateWithServerError}.
172+
*
173+
* @param {string} message A descriptive error message.
174+
*/
175+
constructor(message: string) {
176+
const trueProto = new.target.prototype;
177+
super(message);
178+
this.errorType = 'FailedToNegotiateWithServerError';
179+
180+
// Workaround issue in Typescript compiler
181+
// https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200
182+
this.__proto__ = trueProto;
183+
}
184+
}
185+
161186
/** Error thrown when multiple errors have occurred. */
162187
/** @private */
163188
export class AggregateErrors extends Error {

src/SignalR/clients/ts/signalr/src/HttpConnection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
import { DefaultHttpClient } from "./DefaultHttpClient";
5-
import { AggregateErrors, DisabledTransportError, FailedToStartTransportError, HttpError, UnsupportedTransportError } from "./Errors";
5+
import { AggregateErrors, DisabledTransportError, FailedToNegotiateWithServerError, FailedToStartTransportError, HttpError, UnsupportedTransportError } from "./Errors";
66
import { HeaderNames } from "./HeaderNames";
77
import { HttpClient } from "./HttpClient";
88
import { IConnection } from "./IConnection";
@@ -347,7 +347,7 @@ export class HttpConnection implements IConnection {
347347
}
348348
this._logger.log(LogLevel.Error, errorMessage);
349349

350-
return Promise.reject(new Error(errorMessage));
350+
return Promise.reject(new FailedToNegotiateWithServerError(errorMessage));
351351
}
352352
}
353353

0 commit comments

Comments
 (0)