dotnet/aspnetcore · error · RuntimeException

HubConnection trying to negotiate when not in the CONNECTING

Error message

HubConnection trying to negotiate when not in the CONNECTING state.

What it means

A RuntimeException thrown by startNegotiate if the HubConnection is not in the CONNECTING state when negotiate is attempted. Negotiation is only valid as the first phase of start() (after the state machine moves DISCONNECTED->CONNECTING); reaching startNegotiate in any other state indicates a state-machine violation. This is primarily an internal invariant guard.

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

Solutions

  1. Avoid calling start() and stop() concurrently; serialize lifecycle calls or gate them with a single dispatcher.
  2. Ensure reconnect logic checks getConnectionState() before retrying start().
  3. If this appears in normal usage, capture the concurrent call sequence and report it as a library bug.

Example fix

// before: racing start and stop on different threads
new Thread(() -> connection.start().blockingAwait()).start();
new Thread(() -> connection.stop().blockingAwait()).start();

// after: serialize lifecycle transitions
synchronized (lifecycleLock) {
    connection.start().blockingAwait();
}
// later, on a separate but coordinated path
synchronized (lifecycleLock) {
    connection.stop().blockingAwait();
}
Defensive patterns

Strategy: validation

Validate before calling

// Serialize lifecycle transitions to avoid concurrent start/stop.
if (connection.getConnectionState() == HubConnectionState.DISCONNECTED) {
    connection.start().blockingAwait(); // single-threaded, no concurrent stop
}

Try / catch

try {
    connection.start().blockingAwait();
} catch (RuntimeException e) {
    if (e.getMessage().contains("CONNECTING state")) {
        // a concurrent lifecycle call raced; serialize and retry once
    } else { throw e; }
}

Prevention

When it happens

Trigger: startNegotiate is invoked (via the Single.defer chain in start()) after the state has already transitioned away from CONNECTING, e.g. due to a concurrent stop(), a reconnect race, or a manual state mutation in a test. Practically unreachable in normal single-threaded use but possible under concurrent start()/stop() calls.

Common situations: Concurrent calls to start() and stop() racing. A reconnect/retry loop that triggers negotiation after the connection was torn down. Test code that manipulates state directly. An internal bug where startNegotiate is scheduled but executes after stopConnection already moved state to DISCONNECTED.

Related errors


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