OrchardCMS/OrchardCore · 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

HttpConnection.startInternal only skips server negotiation when options.skipNegotiation is true AND the resolved transport is exactly WebSockets. Any other combination (skipNegotiation with SSE or LongPolling, or with an unresolved/default transport) throws this error, because only WebSockets can connect directly to a hub endpoint without a negotiate handshake.

Solutions

  1. Set transport: signalR.HttpTransportType.WebSockets together with skipNegotiation: true.
  2. Alternatively remove skipNegotiation: true and let the standard negotiate flow run.
  3. Ensure the server also has negotiation enabled if you keep skipNegotiation off.
  4. If the server only supports non-WebSocket transports, do not skip negotiation.

Example fix

// before
new signalR.HubConnectionBuilder()
  .withUrl("/hub", { skipNegotiation: true }) // transport not pinned
  .build();
// after
new signalR.HubConnectionBuilder()
  .withUrl("/hub", {
    skipNegotiation: true,
    transport: signalR.HttpTransportType.WebSockets
  })
  .build();
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

function canSkipNegotiation(options) {
  return options.skipNegotiation === true &&
         options.transport === signalR.HttpTransportType.WebSockets;
}

Try / catch

try {
  await connection.start();
} catch (err) {
  if (err.message.includes("Negotiation can only be skipped")) {
    console.error("Add transport: HttpTransportType.WebSockets when skipNegotiation is true");
  } else { throw err; }
}

Prevention

When it happens

Trigger: Calling withUrl(url, { skipNegotiation: true }) while options.transport is HttpTransportType.ServerSentEvents, HttpTransportType.LongPolling, left as default (auto-negotiate), or set to a non-WebSocket combination.

Common situations: Developers enabling skipNegotiation to work around sticky-session/load-balancer issues without pinning transport: WebSockets; Azure App Service or environments where SSE is the negotiated transport; copied config from a WebSockets-only sample applied to an auto transport.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/7af410b37a1f929e. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore.Modules/OrchardCore.SignalR/wwwroot/Scripts/signalr.js:2834

        }
    }
    async _startInternal(transferFormat) {
        // Store the original base url and the access token factory since they may change
        // as part of negotiating
        let url = this.baseUrl;
        this._accessTokenFactory = this._options.accessTokenFactory;
        this._httpClient._accessTokenFactory = this._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 = 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 === "Disconnecting" /* ConnectionState.Disconnecting */ || this._connectionState === "Disconnected" /* ConnectionState.Disconnected */) {
                        throw new AbortError("The connection was stopped during negotiation.");
                    }
                    if (negotiateResponse.error) {
                        throw new Error(negotiateResponse.error);
                    }
                    if (negotiateResponse.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) {

View on GitHub (pinned to 4306c0717f)