dotnet/aspnetcore · error · Error

Negotiation can only be skipped when using the WebSocket…

Error message

Negotiation can only be skipped when using the WebSocket transport directly.

What it means

When skipNegotiation is true, SignalR can only skip the /negotiate round-trip for the WebSocket transport, because WebSockets are a single upgraded connection. For ServerSentEvents and LongPolling, negotiation is mandatory to obtain a connectionId and transport token. The client enforces this invariant by throwing if skipNegotiation is combined with a non-WebSocket transport.

Solutions

  1. Either remove skipNegotiation (let negotiation run), or set transport to HttpTransportType.WebSockets when keeping skipNegotiation.
  2. Confirm the server actually supports WebSockets (enabled in negotiate response transports list) before using skipNegotiation.
  3. If you must use SSE/LongPolling, never set skipNegotiation: true.

Example fix

// before
.withUrl(url, {
  skipNegotiation: true,
  transport: signalR.HttpTransportType.ServerSentEvents,
})

// after — option A: drop skipNegotiation
.withUrl(url, { transport: signalR.HttpTransportType.ServerSentEvents })
// after — option B: keep skipNegotiation, use WebSockets
.withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })
Defensive patterns

Strategy: validation

Validate before calling

import { HttpTransportType } from "@microsoft/signalr";
function assertSkipNegotiationCompatible(opts: signalR.IHttpConnectionOptions): void {
  if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {
    throw new Error("skipNegotiation requires transport === WebSockets");
  }
}

Type guard

function canSkipNegotiation(opts: signalR.IHttpConnectionOptions): boolean {
  return !opts.skipNegotiation || opts.transport === signalR.HttpTransportType.WebSockets;
}

Try / catch

try { await connection.start(); }
catch (e) {
  if (e instanceof Error && /Negotiation can only be skipped/.test(e.message)) {
    // drop skipNegotiation or switch transport
    opts.skipNegotiation = false;
    return newConnection(opts);
  }
  throw e;
}

Prevention

When it happens

Trigger: Configuring HttpConnection with { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents } or { skipNegotiation: true, transport: HttpTransportType.LongPolling }. Also triggered if transport is auto-select but the only available transports are SSE/LongPolling.

Common situations: Developer copied a tutorial that used skipNegotiation with WebSockets but then forced a different transport for a restricted network. Setting skipNegotiation:true as a 'performance' tweak without realizing only WebSockets support it. Auto transport where the server disables WebSockets.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/0ec22952ecf5b772. Report an issue: GitHub.

Appendix: source

Thrown at src/SignalR/clients/ts/signalr/src/HttpConnection.ts:262

        this._connectionToken = undefined;
        this._connectionUrl = undefined;
        this._initialTokenLifetimeInSeconds = undefined;
        this._transportAccessTokenFromServer = false;
        delete this.features.authenticationRefresh;
        this._accessTokenFactory = this._options.accessTokenFactory;
        this._httpClient._accessTokenFactory = this._accessTokenFactory;
        this._httpClient.setRefreshAccessTokenFactory(this._options.accessTokenFactory);

        try {
            if (this._options.skipNegotiation) {
                if (this._options.transport === HttpTransportType.WebSockets) {
                    // No need to add a connection ID in this case
                    this.transport = this._constructTransport(HttpTransportType.WebSockets);
                    // We should just call connect directly in this case.
                    // No fallback or negotiate in this case.
                    await this._startTransport(url, transferFormat);
                } else {
                    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.");

View on GitHub (pinned to 3600ca084e)