diff --git a/src/SignalR/clients/ts/common/package-lock.json b/src/SignalR/clients/ts/common/package-lock.json index 8b3564efc7..64f49215ce 100644 --- a/src/SignalR/clients/ts/common/package-lock.json +++ b/src/SignalR/clients/ts/common/package-lock.json @@ -2622,14 +2622,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2644,20 +2642,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2774,8 +2769,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2787,7 +2781,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2802,7 +2795,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2810,14 +2802,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2836,7 +2826,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2917,8 +2906,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2930,7 +2918,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3052,7 +3039,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/SignalR/clients/ts/signalr/src/HttpConnection.ts b/src/SignalR/clients/ts/signalr/src/HttpConnection.ts index 470c33e339..d3521f4f85 100644 --- a/src/SignalR/clients/ts/signalr/src/HttpConnection.ts +++ b/src/SignalR/clients/ts/signalr/src/HttpConnection.ts @@ -269,29 +269,34 @@ export class HttpConnection implements IConnection { return; } + const transportExceptions: any[] = []; const transports = negotiateResponse.availableTransports || []; for (const endpoint of transports) { - this.connectionState = ConnectionState.Connecting; - const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); - if (typeof transport === "number") { - this.transport = this.constructTransport(transport); - if (!negotiateResponse.connectionId) { - negotiateResponse = await this.getNegotiationResponse(url); - connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId); - } - try { + try { + this.connectionState = ConnectionState.Connecting; + const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); + if (typeof transport === "number") { + this.transport = this.constructTransport(transport); + if (!negotiateResponse.connectionId) { + negotiateResponse = await this.getNegotiationResponse(url); + connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId); + } await this.transport!.connect(connectUrl, requestedTransferFormat); this.changeState(ConnectionState.Connecting, ConnectionState.Connected); return; - } catch (ex) { - this.logger.log(LogLevel.Error, `Failed to start the transport '${HttpTransportType[transport]}': ${ex}`); - this.connectionState = ConnectionState.Disconnected; - negotiateResponse.connectionId = undefined; } + } catch (ex) { + this.logger.log(LogLevel.Error, `Failed to start the transport '${endpoint.transport}': ${ex}`); + this.connectionState = ConnectionState.Disconnected; + negotiateResponse.connectionId = undefined; + transportExceptions.push(`${endpoint.transport} failed: ${ex}`); } } - throw new Error("Unable to initialize any of the available transports."); + if (transportExceptions.length > 0) { + throw new Error(`Unable to connect to the server with any of the available transports. ${transportExceptions.join(" ")}`); + } + throw new Error("None of the transports supported by the client are supported by the server."); } private constructTransport(transport: HttpTransportType) { @@ -324,15 +329,18 @@ export class HttpConnection implements IConnection { if ((transport === HttpTransportType.WebSockets && !this.options.WebSocket) || (transport === HttpTransportType.ServerSentEvents && !this.options.EventSource)) { this.logger.log(LogLevel.Debug, `Skipping transport '${HttpTransportType[transport]}' because it is not supported in your environment.'`); + throw new Error(`'${HttpTransportType[transport]}' is not supported in your environment.`); } else { this.logger.log(LogLevel.Debug, `Selecting transport '${HttpTransportType[transport]}'.`); return transport; } } else { this.logger.log(LogLevel.Debug, `Skipping transport '${HttpTransportType[transport]}' because it does not support the requested transfer format '${TransferFormat[requestedTransferFormat]}'.`); + throw new Error(`'${HttpTransportType[transport]}' does not support ${TransferFormat[requestedTransferFormat]}.`); } } else { this.logger.log(LogLevel.Debug, `Skipping transport '${HttpTransportType[transport]}' because it was disabled by the client.`); + throw new Error(`'${HttpTransportType[transport]}' is disabled by the client.`); } } return null; diff --git a/src/SignalR/clients/ts/signalr/tests/HttpConnection.test.ts b/src/SignalR/clients/ts/signalr/tests/HttpConnection.test.ts index 4ec762ccc9..f61e4b96e2 100644 --- a/src/SignalR/clients/ts/signalr/tests/HttpConnection.test.ts +++ b/src/SignalR/clients/ts/signalr/tests/HttpConnection.test.ts @@ -171,9 +171,9 @@ describe("HttpConnection", () => { await expect(connection.start(TransferFormat.Text)) .rejects - .toThrow("Unable to initialize any of the available transports."); + .toThrow("None of the transports supported by the client are supported by the server."); }, - "Failed to start the connection: Error: Unable to initialize any of the available transports."); + "Failed to start the connection: Error: None of the transports supported by the client are supported by the server."); }); it("preserves user's query string", async () => { @@ -273,9 +273,11 @@ describe("HttpConnection", () => { await expect(connection.start(TransferFormat.Text)) .rejects - .toThrow("Unable to initialize any of the available transports."); + .toThrow(`Unable to connect to the server with any of the available transports. ${negotiateResponse.availableTransports[0].transport} failed: Error: '${negotiateResponse.availableTransports[0].transport}' is disabled by the client.` + + ` ${negotiateResponse.availableTransports[1].transport} failed: Error: '${negotiateResponse.availableTransports[1].transport}' is disabled by the client.`); }, - "Failed to start the connection: Error: Unable to initialize any of the available transports."); + /Failed to start the connection: Error: Unable to connect to the server with any of the available transports. [a-zA-Z]+\b failed: Error: '[a-zA-Z]+\b' is disabled by the client. [a-zA-Z]+\b failed: Error: '[a-zA-Z]+\b' is disabled by the client./, + /Failed to start the transport '[a-zA-Z]+': Error: '[a-zA-Z]+' is disabled by the client./); }); it(`cannot be started if server's only transport (${HttpTransportType[requestedTransport]}) is masked out by the transport option`, async () => { @@ -311,10 +313,11 @@ describe("HttpConnection", () => { await connection.start(TransferFormat.Text); fail("Expected connection.start to throw!"); } catch (e) { - expect(e.message).toBe("Unable to initialize any of the available transports."); + expect(e.message).toBe(`Unable to connect to the server with any of the available transports. ${HttpTransportType[requestedTransport]} failed: Error: '${HttpTransportType[requestedTransport]}' is disabled by the client.`); } }, - "Failed to start the connection: Error: Unable to initialize any of the available transports."); + `Failed to start the connection: Error: Unable to connect to the server with any of the available transports. ${HttpTransportType[requestedTransport]} failed: Error: '${HttpTransportType[requestedTransport]}' is disabled by the client.`, + `Failed to start the transport '${HttpTransportType[requestedTransport]}': Error: '${HttpTransportType[requestedTransport]}' is disabled by the client.`); }); }); @@ -378,9 +381,9 @@ describe("HttpConnection", () => { const connection = new HttpConnection("http://tempuri.org", options); await expect(connection.start(TransferFormat.Text)) .rejects - .toThrow("Unable to initialize any of the available transports."); + .toThrow("None of the transports supported by the client are supported by the server."); }, - "Failed to start the connection: Error: Unable to initialize any of the available transports."); + "Failed to start the connection: Error: None of the transports supported by the client are supported by the server."); }); it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => { @@ -694,12 +697,12 @@ describe("HttpConnection", () => { await expect(connection.start(TransferFormat.Text)) .rejects - .toEqual(new Error("Unable to initialize any of the available transports.")); + .toEqual(new Error("Unable to connect to the server with any of the available transports. ServerSentEvents failed: Error: EventSource constructor called.")); expect(eventSourceConstructorCalled).toEqual(true); }, "Failed to start the transport 'ServerSentEvents': Error: EventSource constructor called.", - "Failed to start the connection: Error: Unable to initialize any of the available transports."); + "Failed to start the connection: Error: Unable to connect to the server with any of the available transports. ServerSentEvents failed: Error: EventSource constructor called."); }); it("uses WebSocket constructor from options if provided", async () => { diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs index bd9010f9b7..84b9655d10 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs @@ -294,6 +294,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Client // Set the initial access token provider back to the original one from options _accessTokenProvider = _httpConnectionOptions.AccessTokenProvider; + var transportExceptions = new List(); + if (_httpConnectionOptions.SkipNegotiation) { if (_httpConnectionOptions.Transports == HttpTransportType.WebSockets) @@ -349,12 +351,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Client if (!Enum.TryParse(transport.Transport, out var transportType)) { Log.TransportNotSupported(_logger, transport.Transport); + transportExceptions.Add(new TransportFailedException(transport.Transport, "The transport is not supported by the client.")); continue; } if (transportType == HttpTransportType.WebSockets && !IsWebSocketsSupported()) { Log.WebSocketsNotSupportedByOperatingSystem(_logger); + transportExceptions.Add(new TransportFailedException("WebSockets", "The transport is not supported on this operating system.")); continue; } @@ -363,10 +367,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Client if ((transportType & _httpConnectionOptions.Transports) == 0) { Log.TransportDisabledByClient(_logger, transportType); + transportExceptions.Add(new TransportFailedException(transportType.ToString(), "The transport is disabled by the client.")); } else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal)) { Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat); + transportExceptions.Add(new TransportFailedException(transportType.ToString(), $"The transport does not support the '{transferFormat}' transfer format.")); } else { @@ -385,6 +391,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client catch (Exception ex) { Log.TransportFailed(_logger, transportType, ex); + + transportExceptions.Add(new TransportFailedException(transportType.ToString(), ex.Message, ex)); + // Try the next transport // Clear the negotiation response so we know to re-negotiate. negotiationResponse = null; @@ -394,7 +403,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Client if (_transport == null) { - throw new InvalidOperationException("Unable to connect to the server with any of the available transports."); + if (transportExceptions.Count > 0) + { + throw new AggregateException("Unable to connect to the server with any of the available transports.", transportExceptions); + } + else + { + throw new NoTransportSupportedException("None of the transports supported by the client are supported by the server."); + } } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/NoTransportSupportedException.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/NoTransportSupportedException.cs new file mode 100644 index 0000000000..9009ac3c03 --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/NoTransportSupportedException.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http.Connections.Client +{ + /// + /// Exception thrown during negotiate when there are no supported transports between the client and server. + /// + public class NoTransportSupportedException : Exception + { + public NoTransportSupportedException(string message) + : base(message) + { + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/TransportFailedException.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/TransportFailedException.cs new file mode 100644 index 0000000000..4de0804637 --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/TransportFailedException.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Http.Connections.Client +{ + /// + /// Exception thrown during negotiate when a transport fails to connect. + /// + public class TransportFailedException : Exception + { + public string TransportType { get; } + + public TransportFailedException(string transportType, string message, Exception innerException = null) + : base($"{transportType} failed: {message}", innerException) + { + TransportType = transportType; + } + } +} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs index ae73e83aec..c141ffbe8a 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs @@ -154,8 +154,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests transport: new TestTransport(onTransportStart: OnTransportStart)), async (connection) => { - var ex = await Assert.ThrowsAsync(() => connection.StartAsync(TransferFormat.Text)); - Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message); + var ex = await Assert.ThrowsAsync(() => connection.StartAsync(TransferFormat.Text)); + Assert.Equal("Unable to connect to the server with any of the available transports. " + + "(WebSockets failed: Transport failed to start) (ServerSentEvents failed: Transport failed to start) (LongPolling failed: Transport failed to start)", + ex.Message); // If websockets aren't supported then we expect one less attmept to start. if (!TestHelpers.IsWebSocketsSupported()) @@ -343,7 +345,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse), async (connection) => { - await Assert.ThrowsAsync( + await Assert.ThrowsAsync( () => connection.StartAsync(TransferFormat.Text).OrTimeout()); }); } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs index 89893a7e19..db18ec12e5 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs @@ -37,17 +37,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } [Fact] - public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports() + public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer() { - return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: 0), "Unable to connect to the server with any of the available transports."); + return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: HttpTransportType.ServerSentEvents), + "Unable to connect to the server with any of the available transports. (ServerSentEvents failed: The transport is disabled by the client.)"); } - [Theory] - [InlineData(HttpTransportType.None)] - [InlineData(HttpTransportType.ServerSentEvents)] - public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer(HttpTransportType serverTransports) + [Fact] + public Task ConnectionCannotBeStartedIfNoTransportProvidedByServer() { - return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: serverTransports), "Unable to connect to the server with any of the available transports."); + return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: HttpTransportType.None), "None of the transports supported by the client are supported by the server."); } [Theory]