dotnet/aspnetcore · error · Error
Detected a connection attempt to an ASP.NET SignalR Server…
Error message
Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.
What it means
If the negotiate response contains a ProtocolVersion field, the client infers it is talking to a legacy ASP.NET SignalR server (which uses ProtocolVersion), not ASP.NET Core SignalR. The two protocols are incompatible, so the client throws a descriptive error pointing at the migration docs.
Solutions
- Point the Core client at an ASP.NET Core SignalR hub (Microsoft.AspNetCore.SignalR), not a legacy ASP.NET hub.
- If you must talk to a legacy hub, use the legacy microsoft.aspnet.signalr.client JS package instead of @microsoft/signalr.
- Verify the route: ASP.NET Core hubs negotiate at /<hub>/negotiate and return connectionId, not ProtocolVersion.
- Update the reverse-proxy routing so Core traffic goes to the Core server.
Example fix
// before: client is @microsoft/signalr pointed at a legacy hub
const conn = new signalR.HubConnectionBuilder()
.withUrl("https://old-site/signalr") // legacy ASP.NET hub
.build();
// after: use the legacy client for a legacy hub
// npm install microsoft.aspnet.signalr.client
// or migrate the server to ASP.NET Core SignalR and keep @microsoft/signalr Defensive patterns
Strategy: validation
Validate before calling
async function isCoreSignalR(url: string): Promise<boolean> {
const r = await fetch(`${url}/negotiate?negotiateVersion=1`, { method: "POST" });
const body = await r.json().catch(() => ({}));
return !("ProtocolVersion" in body);
} Type guard
function isLegacyNegotiate(v: unknown): boolean {
return typeof v === "object" && v !== null && "ProtocolVersion" in v;
} Try / catch
try { await connection.start(); }
catch (e) {
if (e instanceof Error && /ASP.NET SignalR Server/.test(e.message)) {
console.error("Target is legacy ASP.NET SignalR — use the legacy client or migrate the server.");
}
throw e;
} Prevention
- Confirm the server is ASP.NET Core SignalR before wiring the Core client.
- For legacy hubs, install microsoft.aspnet.signalr.client instead.
- Audit reverse-proxy routing during migration so legacy/Core traffic is split correctly.
When it happens
Trigger: The client's /negotiate response includes a ProtocolVersion property — the hallmark of the older ASP.NET (System.Web / OWIN) SignalR server. Reaches this throw immediately after the negotiate JSON is parsed.
Common situations: Migrating an app: the new Core client is pointed at an old ASP.NET hub still running on the same domain. Reverse proxy routes /hubs/* to a legacy server. Developer confused about which SignalR generation their server runs.
Related errors
- Expected a handshake response from the server.
- Invalid headers.
- Invalid JS call result type
- Negotiate redirection limit exceeded.
- negotiateResponse.error
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/5a41e0067102844c.
Report an issue: GitHub.
Appendix: source
Thrown at src/SignalR/clients/ts/signalr/src/HttpConnection.ts:280
throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");
}
} else {
let negotiateResponse: INegotiateResponse | null = null;
let redirects = 0;
do {
negotiateResponse = await this._getNegotiationResponse(url);
// the user tries to stop the connection when it is being started
if (this._connectionState === ConnectionState.Disconnecting || this._connectionState === ConnectionState.Disconnected) {
throw new AbortError("The connection was stopped during negotiation.");
}
if (negotiateResponse.error) {
throw new Error(negotiateResponse.error);
}
if ((negotiateResponse as any).ProtocolVersion) {
throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");
}
if (negotiateResponse.url) {
url = negotiateResponse.url;
}
if (negotiateResponse.accessToken) {
// Replace the current access token factory with one that uses
// the returned access token
this._setTransportAccessToken(negotiateResponse.accessToken);
}
redirects++;
}
while (negotiateResponse.url && redirects < MAX_REDIRECTS);
if (redirects === MAX_REDIRECTS && negotiateResponse.url) {
throw new Error("Negotiate redirection limit exceeded.");View on GitHub (pinned to 3600ca084e)