dotnet/aspnetcore · error · RuntimeException

HubConnection trying to negotiate when not in the…

Error message

HubConnection trying to negotiate when not in the CONNECTING state.

What it means

Thrown by startNegotiate when the HubConnection's state is not CONNECTING at the moment negotiate is attempted. This is a state-machine invariant: start() transitions to CONNECTING and only then should call startNegotiate. Seeing this throw means the flow reached negotiate from an unexpected state - a coding error inside the client flow or a race that mutated state out from under the negotiate step.

Solutions

  1. Serialize connection lifecycle calls: do not call stop() (or a second start()) while start() is in flight; chain them with the Single/Completable the API returns.
  2. If using an automatic reconnect policy, review its trigger so it does not re-enter negotiate while the original start() is unresolved.
  3. Upgrade the client - races around the CONNECTING state are typically fixed in patch releases; pin to the latest matching version.

Example fix

// before
conn.start().subscribe();
conn.stop().blockingAwait(); // races with start's negotiate -> can throw

// after
conn.start().blockingAwait();
conn.stop().blockingAwait();
Defensive patterns

Strategy: validation

Validate before calling

void safeStart(HubConnection conn) {
  if (conn.getConnectionState() != HubConnectionState.DISCONNECTED) {
    throw new IllegalStateException("already connecting/connected");
  }
  conn.start().blockingAwait();
}

Type guard

boolean isDisconnected(HubConnection conn) {
  return conn.getConnectionState() == HubConnectionState.DISCONNECTED;
}

Try / catch

try {
  conn.start().blockingAwait();
} catch (RuntimeException e) {
  if ("HubConnection trying to negotiate when not in the CONNECTING state.".equals(e.getMessage())) {
    // serialize lifecycle calls; do not call stop()/start() concurrently
  } else throw e;
}

Prevention

When it happens

Trigger: Internal: start()'s flow called startNegotiate after the connection already left CONNECTING (e.g. a concurrent stop()/reconnect raced and moved state to DISCONNECTED or CONNECTED). The user-facing trigger is usually calling stop() during start(), or a reconnect kicking off negotiate on an already-connected hub.

Common situations: Concurrent start()/stop() on the same HubConnection; a reconnect policy that fires while the initial start is still negotiating; calling start() twice without stop() in between on a buggy state path.

Related errors


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

Appendix: source

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

                    }
                // this error is already logged and we want the user to see the original error
                } catch (Exception ex) {
                } finally {
                    this.state.unlock();
                }

                localStart.onError(error);
            });
        } finally {
            this.state.lock.unlock();
        }

        return localStart;
    }

    private Single<NegotiateResponse> startNegotiate(String url, int negotiateAttempts, Map<String, String> localHeaders) {
        if (this.state.getHubConnectionState() != HubConnectionState.CONNECTING) {
            throw new RuntimeException("HubConnection trying to negotiate when not in the CONNECTING state.");
        }

        return handleNegotiate(url, localHeaders).flatMap(response -> {
            if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) {
                throw new RuntimeException("Negotiate redirection limit exceeded.");
            }

            if (response.getRedirectUrl() == null) {
                Set<String> transports = response.getAvailableTransports();
                if (this.transportEnum == TransportEnum.ALL) {
                    if (transports.contains("WebSockets")) {
                        response.setChosenTransport(TransportEnum.WEBSOCKETS);
                    } else if (transports.contains("LongPolling")) {
                        response.setChosenTransport(TransportEnum.LONG_POLLING);
                    } else {
                        throw new RuntimeException("There were no compatible transports on the server.");
                    }
                } else if (this.transportEnum == TransportEnum.WEBSOCKETS && !transports.contains("WebSockets") ||

View on GitHub (pinned to 3600ca084e)