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
- 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.
- If using an automatic reconnect policy, review its trigger so it does not re-enter negotiate while the original start() is unresolved.
- 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
- Serialize start/stop/reconnect; never invoke them concurrently on one HubConnection.
- Chain lifecycle on the returned Single/Completable rather than fire-and-forget.
- Keep the client version current - CONNECTING-state races are fixed in patch releases.
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
- The HubConnection must be in the disconnected state to…
- The HubConnection failed to transition from the
- A valid url is required.
- A valid url is required.
- Cannot handle type class:
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)