dotnet/aspnetcore · error · HttpRequestException

Unexpected status code returned from negotiate

Error message

Unexpected status code returned from negotiate: %d %s.

What it means

Thrown inside handleNegotiate when the POST to the /negotiate endpoint returns an HTTP status other than 200. Wrapped as HttpRequestException carrying the status code and status text. The negotiate request is the first thing the client sends during start(); a non-200 here means the server rejected the negotiate handshake before any transport was chosen.

Solutions

  1. Read the status code and text from the exception: 401/403 -> fix the access token provider; 404 -> correct the hub URL/path; 5xx -> server-side investigation.
  2. Confirm the URL ends at the hub endpoint and that the server maps that route to a hub.
  3. Verify the access token provider returns a valid token before each start.
  4. Check the server logs for the matching negotiate request; ensure the reverse proxy forwards POST /negotiate.

Example fix

// before
HubConnection conn = HubConnectionBuilder.create("http://srv/api") // wrong path -> 404
  .build();

// after
HubConnection conn = HubConnectionBuilder.create("http://srv/hubs/chat") // correct hub route
  .withAccessTokenProvider(Single.just(token))
  .build();
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate reachability and auth before start:
// 1) GET {hub} returns 200/401 (not 404) - confirms the route exists.
// 2) Access token provider returns a non-empty token when auth is required.
String url = "http://srv/hubs/chat";

Type guard

boolean isLikelyHubEndpoint(int statusCode) {
  return statusCode == 200 || statusCode == 401 || statusCode == 403;
}

Try / catch

try {
  conn.start().blockingAwait();
} catch (HttpRequestException e) {
  int code = e.getStatusCode(); // or read from the message
  // 401/403 -> fix token provider; 404 -> fix URL; 5xx -> server logs
}

Prevention

When it happens

Trigger: httpClient.post(resolveNegotiateUrl(url)) returns a response whose getStatusCode() != 200 - e.g. 401/403 (auth), 404 (wrong endpoint/path), 500 (server crash), 502/503 (gateway), 426 (protocol upgrade required).

Common situations: Hub URL is wrong (points to a non-hub path, 404); auth token missing/expired (401/403); server/App Service down or misconfigured (500/502/503); CORS/hosting issue blocks negotiate; the endpoint requires a specific negotiate version and rejects v1; reverse proxy not forwarding the negotiate POST.

Related errors


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

Appendix: 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 3600ca084e)