dotnet/aspnetcore · error · HttpRequestException
Unexpected status code returned from negotiate: %d %s.
Error message
Unexpected status code returned from negotiate: %d %s.
What it means
Thrown as HttpRequestException by handleNegotiate when the HTTP POST to the resolved negotiate URL returns any status code other than 200. The negotiate step is the first server contact during start() and determines available transports, connection tokens, and redirect URLs, so any non-200 is fatal. The %d and %s placeholders carry the numeric status code and reason phrase for diagnosis.
Source
Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java:177
this.handshakeResponseTimeout = handshakeResponseTimeout;
}
this.headers = headers;
this.skipNegotiate = skipNegotiate;
this.serverTimeout = serverTimeout;
this.keepAliveInterval = keepAliveInterval;
this.callback = (payload) -> ReceiveLoop(payload);
}
private Single<NegotiateResponse> handleNegotiate(String url, Map<String, String> localHeaders) {
HttpRequest request = new HttpRequest();
request.addHeaders(localHeaders);
return httpClient.post(Negotiate.resolveNegotiateUrl(url, this.negotiateVersion), request).map((response) -> {
if (response.getStatusCode() != 200) {
throw new HttpRequestException(String.format("Unexpected status code returned from negotiate: %d %s.",
response.getStatusCode(), response.getStatusText()), response.getStatusCode());
}
JsonReader reader = new JsonReader(new StringReader(new String(response.getContent().array(), StandardCharsets.UTF_8)));
NegotiateResponse negotiateResponse = new NegotiateResponse(reader);
if (negotiateResponse.getError() != null) {
throw new RuntimeException(negotiateResponse.getError());
}
if (negotiateResponse.getAccessToken() != null) {
localHeaders.put("Authorization", "Bearer " + negotiateResponse.getAccessToken());
}
return negotiateResponse;
});
}
/**View on GitHub (pinned to 294cab2f9b)
Solutions
- Inspect the status code in the error message; for 401/403 supply or refresh the access token via .withAccessTokenProvider(...).
- For 404, verify the base URL and that the hub is mapped server-side (e.g. MapHub<T> in ASP.NET Core) and that the negotiate endpoint is reachable.
- For 5xx, check the reverse proxy/gateway configuration to ensure negotiate POST requests are forwarded correctly.
- Reproduce the negotiate POST manually (e.g. curl -X POST <url>/negotiate) with the same headers to see the raw server response.
Example fix
// before
HubConnection conn = HubConnectionBuilder.create("https://example.com/hub").build();
conn.start().blockingAwait(); // 401: no auth token
// after
HubConnection conn = HubConnectionBuilder.create("https://example.com/hub")
.withAccessTokenProvider(Single.just(() -> getJwtToken()))
.build();
conn.start().blockingAwait(); Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: hit the negotiate endpoint with the same headers to detect auth/config issues.
// (Illustrative; use your HTTP client.)
int status = httpPost(negotiateUrl, headers).getStatusCode();
if (status != 200) {
throw new IllegalStateException("Negotiate pre-flight failed: " + status);
} Try / catch
connection.start()
.subscribe(() -> { /* connected */ },
error -> {
if (error instanceof HttpRequestException) {
int code = ((HttpRequestException) error).getStatusCode();
// handle 401/403 -> refresh token; 404 -> fix URL; 5xx -> retry/backoff
}
}); Prevention
- Use .withAccessTokenProvider(Single.defer(() -> Single.just(refreshToken()))) so tokens refresh on each connect.
- Verify the hub endpoint and negotiate path are reachable (curl the negotiate URL in CI).
- Configure reverse proxies (nginx/IIS) to forward negotiate and WebSocket upgrade correctly.
- Instrument start() failures by status code to distinguish auth vs routing vs server errors.
When it happens
Trigger: connection.start() triggers a POST to {baseUrl}/negotiate and the server responds 401/403 (auth), 404 (wrong endpoint or missing SignalR mapping), 500 (server error), 502/503 (reverse proxy/gateway failure), or any other non-200. This surfaces as an onError on the start() Completable.
Common situations: Authentication token missing/expired (401/403). The base URL points to the wrong path or SignalR hub is not registered server-side (404). A reverse proxy (nginx, IIS ARR) in front strips or mishandles the negotiate subrequest (502/504). CORS or cross-origin restrictions at the negotiate stage. Server-side rate limiting returning 429. ASP.NET Core SignalR not configured or wrong version mismatch.
Related errors
- Negotiate redirection limit exceeded.
- Error reading NegotiateResponse
- The server responded with status ${response.status}.
- Negotiation can only be skipped when using the WebSocket tra
- HubConnection trying to negotiate when not in the CONNECTING
AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06).
Data as JSON: /api/errors/748b741377db8773.
Report an issue: GitHub.