dotnet/aspnetcore · error · Error

Negotiation can only be skipped when using the WebSocket tra

Error message

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

What it means

Thrown from _startInternal (HttpConnection.ts:262) when `skipNegotiation: true` is set in IHttpConnectionOptions but the requested transport is not WebSockets. Negotiation can only be skipped for the WebSocket transport because it does not need the connection token that negotiate provides; all other transports (ServerSentEvents, LongPolling) require the token returned by negotiate to construct their connect URL.

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 294cab2f9b)

Solutions

  1. Set `skipNegotiation: true` ONLY together with `transport: HttpTransportType.WebSockets`.
  2. Remove `skipNegotiation` from the options when using ServerSentEvents or LongPolling.
  3. If you need SSE/LongPolling, leave negotiate enabled so the connection token is fetched.

Example fix

// before
new HubConnectionBuilder()
  .withUrl(url, { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents })
  .build();

// after
new HubConnectionBuilder()
  .withUrl(url, { transport: HttpTransportType.ServerSentEvents })
  .build();
Defensive patterns

Strategy: validation

Validate before calling

function assertSkipNegotiationValid(opts) {
  if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {
    throw new Error('skipNegotiation requires transport=WebSockets');
  }
}
assertSkipNegotiationValid(options);

Type guard

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

Try / catch

try { await conn.start(transferFormat); }
catch (e) {
  if (/Negotiation can only be skipped/.test(String(e))) {
    options.skipNegotiation = false;
    return new HttpConnection(url, options).start(transferFormat);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `withUrl(url, { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents })` or `LongPolling`, or setting `skipNegotiation: true` with `transport` undefined while the server's first available transport happens to be non-WebSocket (though in that path negotiate runs anyway). The literal throw fires when the explicit transport option is not WebSockets.

Common situations: Copy-pasting a config that used WebSockets and forgetting to remove skipNegotiation after switching to SSE/LongPolling; trying to reduce latency by skipping negotiate without understanding it only works for WebSockets; mixing options objects across transports.

Related errors


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