dotnet/aspnetcore · error · RuntimeException

Negotiation can only be skipped when using the WebSocket…

Error message

Negotiation can only be skipped when using the WebSocket transport directly with '.withTransport(TransportEnum.WEBSOCKETS)' on the 'HubConnectionBuilder'.

What it means

Thrown during start() when skipNegotiate is true but the configured transportEnum is not WEBSOCKETS. Negotiation can be skipped only when connecting straight to WebSockets, because the negotiate response is what tells the client which of the other transports (LongPolling) the server supports. Any other transport requires the negotiate handshake first.

Solutions

  1. If you must skip negotiate, also pin the transport: .withTransport(TransportEnum.WEBSOCKETS).withSkipNegotiate(true).
  2. Otherwise, allow negotiate: do not call withSkipNegotiate(true) when using LongPolling or ALL.
  3. Re-read the builder chain and confirm skipNegotiate and transportEnum are consistent.

Example fix

// before
HubConnection conn = HubConnectionBuilder.create(url)
  .withTransport(TransportEnum.LONG_POLLING)
  .withSkipNegotiate(true) // throws on start()
  .build();

// after (option A: keep skip, pin websockets)
HubConnection conn = HubConnectionBuilder.create(url)
  .withTransport(TransportEnum.WEBSOCKETS)
  .withSkipNegotiate(true)
  .build();

// after (option B: keep long polling, run negotiate)
HubConnection conn = HubConnectionBuilder.create(url)
  .withTransport(TransportEnum.LONG_POLLING)
  .build(); // skipNegotiate defaults to false
Defensive patterns

Strategy: validation

Validate before calling

HubConnection build(boolean skipNegotiate, TransportEnum transport, String url) {
  if (skipNegotiate && transport != TransportEnum.WEBSOCKETS) {
    throw new IllegalArgumentException("skipNegotiate requires WebSockets");
  }
  HubConnectionBuilder b = HubConnectionBuilder.create(url);
  b.withTransport(transport);
  if (skipNegotiate) b.withSkipNegotiate(true);
  return b.build();
}

Type guard

boolean skipNegotiateCompatible(TransportEnum t) {
  return t == TransportEnum.WEBSOCKETS;
}

Try / catch

try {
  conn.start().blockingAwait();
} catch (RuntimeException e) {
  if (e.getMessage() != null && e.getMessage().contains("Negotiation can only be skipped")) {
    // either set transport to WEBSOCKETS, or remove withSkipNegotiate(true)
  } else throw e;
}

Prevention

When it happens

Trigger: HubConnectionBuilder is configured with .withTransport(TransportEnum.LONG_POLLING) (or ALL) AND skipNegotiate=true. The branch in start() checks this combination and throws before opening a transport.

Common situations: Copy/paste from a WebSockets-only sample that set skipNegotiate; misconfigured builder that combined .shouldSkipNegotiate() with a non-WebSocket transport; trying to optimize connection time by skipping negotiate but keeping LongPolling as a fallback.

Related errors


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

Appendix: source

Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java:276

                tokenCompletable.onError(error);
            });

            Single<NegotiateResponse> negotiate = null;
            if (!skipNegotiate) {
                negotiate = tokenCompletable.andThen(Single.defer(() -> startNegotiate(baseUrl, 0, localHeaders)));
            } else {
                negotiate = tokenCompletable.andThen(Single.defer(() -> Single.just(new NegotiateResponse(baseUrl))));
            }

            negotiate.flatMapCompletable(negotiateResponse -> {
                logger.debug("Starting HubConnection.");
                Transport transport = customTransport;
                if (transport == null) {
                    Single<String> tokenProvider = negotiateResponse.getAccessToken() != null ? Single.just(negotiateResponse.getAccessToken()) : accessTokenProvider;
                    TransportEnum chosenTransport;
                    if (this.skipNegotiate) {
                        if (this.transportEnum != TransportEnum.WEBSOCKETS) {
                            throw new RuntimeException("Negotiation can only be skipped when using the WebSocket transport directly with '.withTransport(TransportEnum.WEBSOCKETS)' on the 'HubConnectionBuilder'.");
                        }
                        chosenTransport = this.transportEnum;
                    } else {
                        chosenTransport = negotiateResponse.getChosenTransport();
                    }
                    switch (chosenTransport) {
                        case LONG_POLLING:
                            transport = new LongPollingTransport(localHeaders, httpClient, tokenProvider);
                            break;
                        default:
                            transport = new WebSocketTransport(localHeaders, httpClient);
                    }
                }

                connectionState.transport = transport;

                transport.setOnReceive(this.callback);
                transport.setOnClose((message) -> stopConnection(message));

View on GitHub (pinned to 3600ca084e)