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

  1. Point the Core client at an ASP.NET Core SignalR hub (Microsoft.AspNetCore.SignalR), not a legacy ASP.NET hub.
  2. If you must talk to a legacy hub, use the legacy microsoft.aspnet.signalr.client JS package instead of @microsoft/signalr.
  3. Verify the route: ASP.NET Core hubs negotiate at /<hub>/negotiate and return connectionId, not ProtocolVersion.
  4. 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

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


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)