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

  1. Inspect the status code in the error message; for 401/403 supply or refresh the access token via .withAccessTokenProvider(...).
  2. 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.
  3. For 5xx, check the reverse proxy/gateway configuration to ensure negotiate POST requests are forwarded correctly.
  4. 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

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


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