Skip to content

Commit 2dced01

Browse files
[SignalR TS] Improve connection error messages (#31402)
1 parent ab2704a commit 2dced01

13 files changed

+98
-36
lines changed

src/SignalR/clients/ts/FunctionalTests/Startup.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<
197197
await next.Invoke();
198198
});
199199

200+
app.Use((context, next) =>
201+
{
202+
if (context.Request.Path.StartsWithSegments("/bad-negotiate"))
203+
{
204+
context.Response.StatusCode = 400;
205+
return context.Response.WriteAsync("Some response from server");
206+
}
207+
208+
return next();
209+
});
210+
200211
app.UseRouting();
201212

202213
// Custom CORS to allow any origin + credentials (which isn't allowed by the CORS spec)

src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,20 @@ describe("connection", () => {
222222

223223
await closePromise;
224224
});
225+
226+
it("contains server response in error", async () => {
227+
const connection = new HttpConnection(ENDPOINT_BASE_URL + "/bad-negotiate", {
228+
...commonOptions,
229+
httpClient,
230+
});
231+
232+
try {
233+
await connection.start(TransferFormat.Text);
234+
expect(true).toBe(false);
235+
} catch (e) {
236+
expect(e).toEqual(new Error("Failed to complete negotiation with the server: Error: Some response from server: Status code '400'"));
237+
}
238+
})
225239
});
226240
});
227241
});

src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,24 @@ describe("hubConnection", () => {
558558
}
559559
});
560560

561+
it("can get error from unauthorized hub connection", async (done) => {
562+
try {
563+
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/authorizedhub").build();
564+
565+
hubConnection.onclose((error) => {
566+
expect(error).toBe(undefined);
567+
done();
568+
});
569+
570+
await hubConnection.start();
571+
572+
fail("shouldn't reach here");
573+
} catch (err) {
574+
expect(err).toEqual(new Error("Failed to complete negotiation with the server: Error: Unauthorized: Status code '401'"));
575+
done();
576+
}
577+
});
578+
561579
if (transportType !== HttpTransportType.LongPolling) {
562580
it("terminates if no messages received within timeout interval", async (done) => {
563581
const hubConnection = getConnectionBuilder(transportType).build();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class HttpError extends Error {
1717
*/
1818
constructor(errorMessage: string, statusCode: number) {
1919
const trueProto = new.target.prototype;
20-
super(errorMessage);
20+
super(`${errorMessage}: Status code '${statusCode}'`);
2121
this.statusCode = statusCode;
2222

2323
// Workaround issue in Typescript compiler

src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ export class FetchHttpClient extends HttpClient {
113113
}
114114

115115
if (!response.ok) {
116-
throw new HttpError(response.statusText, response.status);
116+
const errorMessage = await deserializeContent(response, "text") as string;
117+
throw new HttpError(errorMessage || response.statusText, response.status);
117118
}
118119

119120
const content = deserializeContent(response, request.responseType);

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

Lines changed: 10 additions & 2 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
import { DefaultHttpClient } from "./DefaultHttpClient";
5+
import { HttpError } from "./Errors";
56
import { HttpClient } from "./HttpClient";
67
import { IConnection } from "./IConnection";
78
import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
@@ -334,8 +335,15 @@ export class HttpConnection implements IConnection {
334335
}
335336
return negotiateResponse;
336337
} catch (e) {
337-
this._logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e);
338-
return Promise.reject(e);
338+
let errorMessage = "Failed to complete negotiation with the server: " + e;
339+
if (e instanceof HttpError) {
340+
if (e.statusCode === 404) {
341+
errorMessage = errorMessage + " Either this is not a SignalR endpoint or there is a proxy blocking the connection.";
342+
}
343+
}
344+
this._logger.log(LogLevel.Error, errorMessage);
345+
346+
return Promise.reject(new Error(errorMessage));
339347
}
340348
}
341349

src/SignalR/clients/ts/signalr/src/ServerSentEventsTransport.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ export class ServerSentEventsTransport implements ITransport {
9090

9191
// @ts-ignore: not using event on purpose
9292
eventSource.onerror = (e: Event) => {
93-
const error = new Error("Error occurred while starting EventSource");
93+
// EventSource doesn't give any useful information about server side closes.
9494
if (opened) {
95-
this._close(error);
95+
this._close();
9696
} else {
97-
reject(error);
97+
reject(new Error("EventSource failed to connect. The connection could not be found on the server,"
98+
+ " either the connection ID is not present on the server, or a proxy is refusing/buffering the connection."
99+
+ " If you have multiple servers check that sticky sessions are enabled."));
98100
}
99101
};
100102

src/SignalR/clients/ts/signalr/src/WebSocketTransport.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ export class WebSocketTransport implements ITransport {
9191
if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
9292
error = event.error;
9393
} else {
94-
error = new Error("There was an error with the transport.");
94+
error = "There was an error with the transport";
9595
}
9696

97-
reject(error);
97+
this._logger.log(LogLevel.Information, `(WebSockets transport) ${error}.`);
9898
};
9999

100100
webSocket.onmessage = (message: MessageEvent) => {
@@ -120,10 +120,13 @@ export class WebSocketTransport implements ITransport {
120120
if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
121121
error = event.error;
122122
} else {
123-
error = new Error("There was an error with the transport.");
123+
error = "WebSocket failed to connect. The connection could not be found on the server,"
124+
+ " either the endpoint may not be a SignalR endpoint,"
125+
+ " the connection ID is not present on the server, or there is a proxy blocking WebSockets."
126+
+ " If you have multiple servers check that sticky sessions are enabled.";
124127
}
125128

126-
reject(error);
129+
reject(new Error(error));
127130
}
128131
};
129132
});
@@ -163,7 +166,7 @@ export class WebSocketTransport implements ITransport {
163166
this._logger.log(LogLevel.Trace, "(WebSockets transport) socket closed.");
164167
if (this.onclose) {
165168
if (this._isCloseEvent(event) && (event.wasClean === false || event.code !== 1000)) {
166-
this.onclose(new Error(`WebSocket closed with status code: ${event.code} (${event.reason}).`));
169+
this.onclose(new Error(`WebSocket closed with status code: ${event.code} (${event.reason || "no reason given"}).`));
167170
} else if (event instanceof Error) {
168171
this.onclose(event);
169172
} else {

src/SignalR/clients/ts/signalr/src/XhrHttpClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class XhrHttpClient extends HttpClient {
6767
if (xhr.status >= 200 && xhr.status < 300) {
6868
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText));
6969
} else {
70-
reject(new HttpError(xhr.statusText, xhr.status));
70+
reject(new HttpError(xhr.response || xhr.responseText || xhr.statusText, xhr.status));
7171
}
7272
};
7373

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ describe("HttpConnection", () => {
6767

6868
await expect(connection.start(TransferFormat.Text))
6969
.rejects
70-
.toBe("error");
70+
.toThrow(new Error("Failed to complete negotiation with the server: error"));
7171
},
72-
"Failed to start the connection: error",
72+
"Failed to start the connection: Error: Failed to complete negotiation with the server: error",
7373
"Failed to complete negotiation with the server: error");
7474
});
7575

@@ -125,14 +125,14 @@ describe("HttpConnection", () => {
125125

126126
await expect(connection.start(TransferFormat.Text))
127127
.rejects
128-
.toBe("reached negotiate.");
128+
.toThrow(new Error("Failed to complete negotiation with the server: reached negotiate."));
129129

130130
await expect(connection.start(TransferFormat.Text))
131131
.rejects
132-
.toBe("reached negotiate.");
132+
.toThrow(new Error("Failed to complete negotiation with the server: reached negotiate."));
133133
},
134134
"Failed to complete negotiation with the server: reached negotiate.",
135-
"Failed to start the connection: reached negotiate.");
135+
"Failed to start the connection: Error: Failed to complete negotiation with the server: reached negotiate.");
136136
});
137137

138138
it("can stop a starting connection", async () => {
@@ -229,14 +229,16 @@ describe("HttpConnection", () => {
229229
const connection = new HttpConnection("http://tempuri.org", options);
230230
await expect(connection.start(TransferFormat.Text))
231231
.rejects
232-
.toThrow("Unable to connect to the server with any of the available transports. WebSockets failed: Error: There was an error with the transport. " +
233-
"ServerSentEvents failed: Error: 'ServerSentEvents' is disabled by the client. LongPolling failed: Error: 'LongPolling' is disabled by the client.");
232+
.toThrow("Unable to connect to the server with any of the available transports. WebSockets failed: Error: WebSocket failed to connect. " +
233+
"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, " +
234+
"or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled. ServerSentEvents failed: Error: 'ServerSentEvents' is disabled by the client. LongPolling failed: Error: 'LongPolling' is disabled by the client.");
234235

235236
expect(negotiateCount).toEqual(1);
236237
},
237-
"Failed to start the transport 'WebSockets': Error: There was an error with the transport.",
238-
"Failed to start the connection: Error: Unable to connect to the server with any of the available transports. WebSockets failed: Error: There was an error with the transport. " +
239-
"ServerSentEvents failed: Error: 'ServerSentEvents' is disabled by the client. LongPolling failed: Error: 'LongPolling' is disabled by the client.");
238+
"Failed to start the transport 'WebSockets': Error: WebSocket failed to connect. The connection could not be found on the server, " +
239+
"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.",
240+
"Failed to start the connection: Error: Unable to connect to the server with any of the available transports. WebSockets failed: " +
241+
"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. ServerSentEvents failed: Error: 'ServerSentEvents' is disabled by the client. LongPolling failed: Error: 'LongPolling' is disabled by the client.");
240242
});
241243

242244
it("negotiate called again when transport fails to start and falls back", async () => {
@@ -300,7 +302,7 @@ describe("HttpConnection", () => {
300302
},
301303
"Failed to start the transport 'WebSockets': Error: Don't allow Websockets.",
302304
"Failed to complete negotiation with the server: Error: negotiate failed",
303-
"Failed to start the connection: Error: negotiate failed");
305+
"Failed to start the connection: Error: Failed to complete negotiation with the server: Error: negotiate failed");
304306
});
305307

306308
it("can stop a non-started connection", async () => {
@@ -399,8 +401,8 @@ describe("HttpConnection", () => {
399401
await connection.stop();
400402
}
401403
},
402-
"Failed to complete negotiation with the server: Error: We don't care how this turns out",
403-
"Failed to start the connection: Error: We don't care how this turns out");
404+
"Failed to complete negotiation with the server: Error: We don't care how this turns out: Status code '500'",
405+
"Failed to start the connection: Error: Failed to complete negotiation with the server: Error: We don't care how this turns out: Status code '500'");
404406
});
405407
});
406408

@@ -1122,7 +1124,7 @@ describe("HttpConnection", () => {
11221124

11231125
await TestWebSocket.webSocketSet;
11241126
await TestWebSocket.webSocket.closeSet;
1125-
TestWebSocket.webSocket.onerror(new TestEvent());
1127+
TestWebSocket.webSocket.onclose(new TestEvent());
11261128

11271129
try {
11281130
await startPromise;
@@ -1133,7 +1135,8 @@ describe("HttpConnection", () => {
11331135

11341136
await connection.stop();
11351137
},
1136-
"Failed to start the transport 'WebSockets': Error: There was an error with the transport.");
1138+
"Failed to start the transport 'WebSockets': Error: WebSocket failed to connect. The connection could not be found on the server, " +
1139+
"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.");
11371140
});
11381141

11391142
it("user agent header set on negotiate", async () => {

src/SignalR/clients/ts/signalr/tests/HubConnection.Reconnect.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,6 @@ describe("auto reconnect", () => {
876876
expect(closeCount).toBe(1);
877877
},
878878
"Failed to complete negotiation with the server: Error with negotiate",
879-
"Failed to start the connection: Error with negotiate");
879+
"Failed to start the connection: Error: Failed to complete negotiation with the server: Error with negotiate");
880880
});
881881
});

src/SignalR/clients/ts/signalr/tests/ServerSentEventsTransport.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe("ServerSentEventsTransport", () => {
6363

6464
await expect(connectPromise)
6565
.rejects
66-
.toEqual(new Error("Error occurred while starting EventSource"));
66+
.toEqual(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."));
6767
expect(closeCalled).toBe(false);
6868
});
6969
});
@@ -164,7 +164,7 @@ describe("ServerSentEventsTransport", () => {
164164

165165
expect(closeCalled).toBe(true);
166166
expect(TestEventSource.eventSource.closed).toBe(true);
167-
expect(error).toEqual(new Error("Error occurred while starting EventSource"));
167+
expect(error).toBeUndefined();
168168
});
169169
});
170170

src/SignalR/clients/ts/signalr/tests/WebSocketTransport.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ describe("WebSocketTransport", () => {
5959

6060
expect(connectComplete).toBe(false);
6161

62-
TestWebSocket.webSocket.onerror(new TestEvent());
62+
TestWebSocket.webSocket.onclose(new TestEvent());
6363

6464
await expect(connectPromise)
6565
.rejects
66-
.toThrow("There was an error with the transport.");
66+
.toThrow("WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, " +
67+
"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.");
6768
expect(connectComplete).toBe(false);
6869
});
6970
});
@@ -89,7 +90,8 @@ describe("WebSocketTransport", () => {
8990

9091
await expect(connectPromise)
9192
.rejects
92-
.toThrow("There was an error with the transport.");
93+
.toThrow("WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, " +
94+
"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.");
9395
expect(connectComplete).toBe(false);
9496
expect(closeCalled).toBe(false);
9597
});
@@ -340,8 +342,8 @@ describe("WebSocketTransport", () => {
340342
expect(closeCalled).toBe(false);
341343
expect(error!).toBeUndefined();
342344

343-
TestWebSocket.webSocket.onerror(new TestEvent());
344-
await expect(connectPromise).rejects.toThrow("There was an error with the transport.");
345+
await expect(connectPromise).rejects.toThrow("WebSocket failed to connect. The connection could not be found on the server, " +
346+
"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.");
345347
});
346348
});
347349
});

0 commit comments

Comments
 (0)