Skip to content

Commit a09fc71

Browse files
Blazor Disable Non-WebSockets Transports by Default (#34644)
* Blazor Disable Non-WebSockets Transports by Default * Update dist .js files * Added UnsupportedTransportWebSocketsError * Added FailedToStartTransportError * E2E Tests * Updated async awaits * Remove ErrorIfBrowserDoesNotSupportWebSockets * MultipleErrors Approach * Always show error notification if `connection.start` throws * PR Feedback * Fix transport failed exception handling * TransportsServerStartup && ErrorIfClientAttemptsWebSocketsWithServerOnLongPolling * Fix Build * Update src/Components/test/testassets/TestServer/Pages/Transports.cshtml Co-authored-by: Pranav K <[email protected]> Co-authored-by: Pranav K <[email protected]>
1 parent 7bf69a2 commit a09fc71

File tree

10 files changed

+309
-12
lines changed

10 files changed

+309
-12
lines changed

src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(
4949
throw new ArgumentNullException(nameof(path));
5050
}
5151

52-
return endpoints.MapBlazorHub(path, configureOptions: _ => { });
52+
// Only support the WebSockets transport type by default
53+
return endpoints.MapBlazorHub(path, configureOptions: options => { options.Transports = HttpTransportType.WebSockets; });
5354
}
5455

5556
/// <summary>

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/dist/Release/blazor.webview.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: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DotNet } from '@microsoft/dotnet-js-interop';
22
import { Blazor } from './GlobalExports';
3-
import { HubConnectionBuilder, HubConnection } from '@microsoft/signalr';
3+
import { HubConnectionBuilder, HubConnection, HttpTransportType } from '@microsoft/signalr';
44
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';
55
import { showErrorNotification } from './BootErrors';
66
import { shouldAutoStart } from './BootCommon';
@@ -85,7 +85,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
8585
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
8686

8787
const connectionBuilder = new HubConnectionBuilder()
88-
.withUrl('_blazor')
88+
.withUrl('_blazor', HttpTransportType.WebSockets)
8989
.withHubProtocol(hubProtocol);
9090

9191
options.configureSignalR(connectionBuilder);
@@ -130,6 +130,17 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
130130
await connection.start();
131131
} catch (ex) {
132132
unhandledError(connection, ex, logger);
133+
134+
if (ex.innerErrors && ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === 'WebSockets')) {
135+
showErrorNotification('Unable to connect, please ensure you are using an updated browser that supports WebSockets.');
136+
} else if (ex.innerErrors && ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === 'WebSockets')) {
137+
showErrorNotification('Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.');
138+
} else if (ex.innerErrors && ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === 'LongPolling')) {
139+
logger.log(LogLevel.Error, 'Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. To troubleshoot this, visit https://aka.ms/blazor-server-websockets-error.');
140+
showErrorNotification();
141+
} else {
142+
showErrorNotification();
143+
}
133144
}
134145

135146
DotNet.attachDispatcher({

src/Components/Web.JS/src/BootErrors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
let hasFailed = false;
22

3-
export async function showErrorNotification() {
3+
export async function showErrorNotification(customErrorMessage: string = '') {
44
let errorUi = document.querySelector('#blazor-error-ui') as HTMLElement;
55
if (errorUi) {
66
errorUi.style.display = 'block';
7+
8+
if (customErrorMessage && errorUi.firstChild) {
9+
errorUi.firstChild.textContent = `\n\t${customErrorMessage}\t\n`;
10+
}
711
}
812

913
if (!hasFailed) {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using System.Threading;
6+
using BasicTestApp;
7+
using BasicTestApp.Reconnection;
8+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
9+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
10+
using Microsoft.AspNetCore.E2ETesting;
11+
using OpenQA.Selenium;
12+
using TestServer;
13+
using Xunit;
14+
using Xunit.Abstractions;
15+
16+
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
17+
{
18+
public class ServerTransportsTest : ServerTestBase<BasicTestAppServerSiteFixture<TransportsServerStartup>>
19+
{
20+
public ServerTransportsTest(
21+
BrowserFixture browserFixture,
22+
BasicTestAppServerSiteFixture<TransportsServerStartup> serverFixture,
23+
ITestOutputHelper output)
24+
: base(browserFixture, serverFixture, output)
25+
{
26+
}
27+
28+
[Fact]
29+
public void DefaultTransportsWorksWithWebSockets()
30+
{
31+
Navigate("/defaultTransport/Transports");
32+
33+
Browser.Exists(By.Id("startBlazorServerBtn")).Click();
34+
35+
var javascript = (IJavaScriptExecutor)Browser;
36+
Browser.True(() => (bool)javascript.ExecuteScript("return window['__aspnetcore__testing__blazor__start__script__executed__'] === true;"));
37+
38+
AssertLogContainsMessages(
39+
"Starting up Blazor server-side application.",
40+
"WebSocket connected to ws://",
41+
"Received render batch with",
42+
"The HttpConnection connected successfully.",
43+
"Blazor server-side application started.");
44+
}
45+
46+
[Fact]
47+
public void ErrorIfClientAttemptsLongPollingWithServerOnWebSockets()
48+
{
49+
Navigate("/defaultTransport/Transports");
50+
51+
Browser.Exists(By.Id("startWithLongPollingBtn")).Click();
52+
53+
var javascript = (IJavaScriptExecutor)Browser;
54+
Browser.True(() => (bool)javascript.ExecuteScript("return window['__aspnetcore__testing__blazor__start__script__executed__'] === true;"));
55+
56+
AssertLogContainsMessages(
57+
"Information: Starting up Blazor server-side application.",
58+
"Failed to start the connection: Error: Unable to connect to the server with any of the available transports.",
59+
"Failed to start the circuit.");
60+
61+
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
62+
Assert.NotNull(errorUiElem);
63+
Assert.Contains("An unhandled exception has occurred. See browser dev tools for details.", errorUiElem.GetAttribute("innerHTML"));
64+
Browser.Equal("block", () => errorUiElem.GetCssValue("display"));
65+
}
66+
67+
[Fact]
68+
public void ErrorIfWebSocketsConnectionIsRejected()
69+
{
70+
Navigate("/defaultTransport/Transports");
71+
72+
Browser.Exists(By.Id("startAndRejectWebSocketConnectionBtn")).Click();
73+
74+
var javascript = (IJavaScriptExecutor)Browser;
75+
Browser.True(() => (bool)javascript.ExecuteScript("return window['__aspnetcore__testing__blazor__start__script__executed__'] === true;"));
76+
77+
AssertLogContainsMessages(
78+
"Information: Starting up Blazor server-side application.",
79+
"Selecting transport 'WebSockets'.",
80+
"Error: Failed to start the transport 'WebSockets': Error: Don't allow Websockets.",
81+
"Error: Failed to start the connection: Error: Unable to connect to the server with any of the available transports. Error: WebSockets failed: Error: Don't allow Websockets.",
82+
"Failed to start the circuit.");
83+
84+
// Ensure error ui is visible
85+
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
86+
Assert.NotNull(errorUiElem);
87+
Assert.Contains("Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.", errorUiElem.GetAttribute("innerHTML"));
88+
Browser.Equal("block", () => errorUiElem.GetCssValue("display"));
89+
}
90+
91+
[Fact]
92+
public void ErrorIfClientAttemptsWebSocketsWithServerOnLongPolling()
93+
{
94+
Navigate("/longPolling/Transports");
95+
96+
Browser.Exists(By.Id("startBlazorServerBtn")).Click();
97+
98+
var javascript = (IJavaScriptExecutor)Browser;
99+
Browser.True(() => (bool)javascript.ExecuteScript("return window['__aspnetcore__testing__blazor__start__script__executed__'] === true;"));
100+
101+
AssertLogContainsMessages(
102+
"Starting up Blazor server-side application.",
103+
"Unable to connect to the server with any of the available transports. LongPolling failed: Error: 'LongPolling' is disabled by the client.",
104+
"Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. To troubleshoot this, visit");
105+
106+
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
107+
Assert.NotNull(errorUiElem);
108+
Assert.Contains("An unhandled exception has occurred. See browser dev tools for details.", errorUiElem.GetAttribute("innerHTML"));
109+
Browser.Equal("block", () => errorUiElem.GetCssValue("display"));
110+
}
111+
112+
void AssertLogContainsMessages(params string[] messages)
113+
{
114+
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
115+
foreach (var message in messages)
116+
{
117+
Assert.Contains(log, entry =>
118+
{
119+
return entry.Message.Contains(message, StringComparison.InvariantCulture);
120+
});
121+
}
122+
}
123+
}
124+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@page
2+
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
3+
4+
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
5+
6+
<div id="blazor-error-ui">
7+
An unhandled exception has occurred. See browser dev tools for details.
8+
<a href="" class="reload">Reload</a>
9+
<a class="dismiss">🗙</a>
10+
</div>
11+
12+
<button id="startBlazorServerBtn" onclick="startBlazorServer()">Start Normally</button>
13+
<button id="startWithLongPollingBtn" onclick="startWithLongPolling()">Start with Long Polling</button>
14+
<button id="startAndRejectWebSocketConnectionBtn" onclick="startAndRejectWebSocketConnection()">Start with WebSockets and Reject Connection</button>
15+
16+
<script src="_framework/blazor.server.js" autostart="false"></script>
17+
<script>
18+
console.log('Blazor server-side');
19+
20+
function startBlazorServer() {
21+
Blazor.start({
22+
logLevel: 1, // LogLevel.Debug
23+
configureSignalR: builder => {
24+
builder.configureLogging("debug") // LogLevel.Debug
25+
}
26+
}).then(function () {
27+
window['__aspnetcore__testing__blazor__start__script__executed__'] = true;
28+
});
29+
}
30+
31+
function startWithLongPolling() {
32+
Blazor.start({
33+
logLevel: 1, // LogLevel.Debug
34+
configureSignalR: builder => {
35+
builder.configureLogging("debug") // LogLevel.Debug
36+
.withUrl('_blazor', 4) // Long Polling (4)
37+
}
38+
}).then(function () {
39+
window['__aspnetcore__testing__blazor__start__script__executed__'] = true;
40+
});
41+
}
42+
43+
function WebSocketNotAllowed() { throw new Error("Don't allow Websockets."); }
44+
45+
function startAndRejectWebSocketConnection() {
46+
Blazor.start({
47+
logLevel: 1, // LogLevel.Debug
48+
configureSignalR: builder => {
49+
builder.configureLogging("debug") // LogLevel.Debug
50+
.withUrl('_blazor', {
51+
WebSocket: WebSocketNotAllowed,
52+
transport: 1, // WebSockets (1)
53+
})
54+
}
55+
}).then(function () {
56+
window['__aspnetcore__testing__blazor__start__script__executed__'] = true;
57+
});
58+
}
59+
</script>

src/Components/test/testassets/TestServer/ServerStartup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void ConfigureServices(IServiceCollection services)
3838
}
3939

4040
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
41-
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ResourceRequestLog resourceRequestLog)
41+
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, ResourceRequestLog resourceRequestLog)
4242
{
4343
var enUs = new CultureInfo("en-US");
4444
CultureInfo.DefaultThreadCurrentCulture = enUs;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Globalization;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.DataProtection;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.Extensions.Configuration;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Hosting;
12+
13+
namespace TestServer
14+
{
15+
public class TransportsServerStartup : ServerStartup
16+
{
17+
public TransportsServerStartup(IConfiguration configuration)
18+
: base (configuration)
19+
{
20+
}
21+
22+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
23+
public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, ResourceRequestLog resourceRequestLog)
24+
{
25+
if (env.IsDevelopment())
26+
{
27+
app.UseDeveloperExceptionPage();
28+
}
29+
30+
app.Map("/defaultTransport", app =>
31+
{
32+
app.UseStaticFiles();
33+
34+
app.UseRouting();
35+
app.UseEndpoints(endpoints =>
36+
{
37+
endpoints.MapBlazorHub();
38+
endpoints.MapFallbackToPage("/_ServerHost");
39+
});
40+
});
41+
42+
app.Map("/longPolling", app =>
43+
{
44+
app.UseStaticFiles();
45+
46+
app.UseRouting();
47+
app.UseEndpoints(endpoints =>
48+
{
49+
endpoints.MapBlazorHub(configureOptions: options =>
50+
{
51+
options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
52+
});
53+
endpoints.MapFallbackToPage("/_ServerHost");
54+
});
55+
});
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)