Skip to content

[Blazor] Error UI is shown on a transient connection failure during reconnection when skipNegotiation is used, and stays after a successful reconnect #69596

Description

@vargavik

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Closest existing issues I found: #58932 has the same console signature (startConnection -> unhandledError) but a different root cause (IIS + WebSocket compression); #25951 / #26174 are the 3.1-era "error UI after inactivity" reports with a different cause.

With skipNegotiation: true (WebSockets-only, no negotiate request), any failure of connection.start() during an automatic reconnection attempt shows #blazor-error-ui ("An unhandled error has occurred. Reload"). The reconnection handler then keeps retrying, the next attempt succeeds, the circuit is resumed (no reload) and the app works again, but the error UI stays until the user dismisses it. The user sees a fatal-looking error for a transient network blip that the framework recovered from.

The cause seems to be the catch block in CircuitManager.startConnection() (src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts, lines 231-244 on main):

try {
  await connection.start();
  this.handleConnectionUp();
} catch (ex: any) {
  this.unhandledError(ex as Error);

  if (ex.errorType === 'FailedToNegotiateWithServerError') {
    // Connection with the server has been interrupted, and we're in the process of reconnecting.
    // Throw this exception so it can be handled at the reconnection layer, and don't show the
    // error notification.
    throw ex;
  } else {
    showErrorNotification();
  }
  ...
}
return connection;

The reconnect case is only recognised via FailedToNegotiateWithServerError. With skipNegotiation: true there is no negotiate request, so a failed reconnection attempt surfaces as a transport error (WebSocket failed to connect, or WebSocket closed with status code: 1006 if the socket dies before the handshake completes). That goes to showErrorNotification(). The error is also not rethrown, so reconnect() goes on to call invoke('ConnectCircuit') on a connection that is not connected, which logs a second error: Cannot send data if the connection is not in the 'Connected' State.

Expected Behavior

A failed connection start during reconnection is handled by the reconnection layer (reconnect modal / retry) and does not show the fatal error UI. This is already the behaviour when negotiation is used.

Possible fix: in startConnection(), when it is called from reconnect() (or the circuit was already started), rethrow instead of calling showErrorNotification(), regardless of the error type.

Steps To Reproduce

The repro is the empty Blazor Server template with one file changed, so it is inlined here rather than in a separate repo (happy to publish one if needed).

  1. dotnet new blazor --interactivity Server --empty -o ReconnectRepro
  2. In Components/App.razor make the app interactive and start Blazor with skipNegotiation:
<Routes @rendermode="InteractiveServer" />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withUrl("_blazor", { skipNegotiation: true, transport: 1 /* WebSockets */ });
      }
    }
  });
</script>
  1. Run the app on http://127.0.0.1:5299 and put a TCP proxy in front of it on 127.0.0.1:5298 (any proxy works; I used a ~20 line Python asyncio one) so the network can be cut while the server and circuit stay alive.
  2. Open http://127.0.0.1:5298/ and wait for WebSocket connected.
  3. Stop the proxy. The reconnect modal appears and the error UI appears at the same time.
  4. Start the proxy again within the circuit retention period. The modal goes away, the circuit is resumed (no page reload; performance.getEntriesByType('navigation')[0].type is still navigate, performance.now() keeps counting), but #blazor-error-ui stays display: block.

Control: the same steps without skipNegotiation: true ({ transport: 1 } only) show only the reconnect modal. The error UI stays hidden because the failure is a FailedToNegotiateWithServerError, which is rethrown.

Stopping the server instead of the proxy also shows the error UI during reconnection (then the circuit is gone and the default ReconnectModal.razor.js reloads the page, so it is less visible).

Exceptions (if any)

Browser console at the moment the connection drops (blazor.web.js from 10.0.12):

Error: Connection disconnected with error 'Error: WebSocket closed with status code: 1006 (no reason given).'.
Information: (WebSockets transport) There was an error with the transport.
Error: Failed to start the connection: Error: WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
Error: Error: WebSocket failed to connect. The connection could not be found on the server, ...        <- unhandledError() in startConnection, error UI shown here
Error: Error: Cannot send data if the connection is not in the 'Connected' State.                   <- reconnect() invoking ConnectCircuit on the failed connection

No server-side exception or log entry. From the server's point of view, it is only a disconnect followed by a reconnect.

.NET Version

10.0.401 (SDK), ASP.NET Core runtime 10.0.12

Anything else?

ASP.NET Core version: 10.0.12 (the code quoted above is unchanged on main)
The IDE: none (dotnet CLI); browser: desktop Chrome on Windows 11

How we hit it in production: Blazor Server app behind nginx and Cloudflare Tunnel, skipNegotiation: true, desktop Chrome. The tab sat in the background for about an hour. Chrome kept closing the WebSocket (WebSocket connection to 'wss://…/_blazor' failed: Page entered Back-Forward Cache.) and Blazor reconnected about once a minute. Most reconnects were clean. In one of them the new socket was closed before the SignalR handshake completed, connection.start() rejected with WebSocket closed with status code: 1006, and the error UI appeared. The next attempt two seconds later succeeded, and the app kept working after the error UI was dismissed.

Activity

  1. added theissue type on Sep 29, 2026
  2. added this to the Backlog milestone on Oct 22, 2026
  3. added
    bugThis issue describes a behavior which is not expected - a bug.
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area-blazorIncludes: Blazor, Razor ComponentsbugThis issue describes a behavior which is not expected - a bug.investigate

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions