dotnet/aspnetcore · error · RuntimeException

Error reading NegotiateResponse

Error message

Error reading NegotiateResponse

What it means

NegotiateResponse parses the JSON body returned by the /negotiate POST. Any IOException while reading the stream (malformed JSON, premature EOF, network read failure mid-parse) is wrapped in a RuntimeException with the generic message. The parser does skip unknown properties for forward compatibility, so only genuine parse/stream errors reach this catch.

Solutions

  1. curl -i the /negotiate URL with the same auth headers and inspect the raw body and status code.
  2. Ensure the hub endpoint is reachable and returns application/json; fix reverse proxy to pass through negotiate responses.
  3. Verify the access token / auth is accepted - a 401 HTML login page will fail to parse as JSON.

Example fix

// before
HubConnection hub = HubConnectionBuilder.create("https://app/hub").build();
hub.start(); // negotiate returns HTML -> parse error

// after - verify negotiate returns JSON
// curl -i -H "Authorization: Bearer ..." https://app/hub/negotiate
// must return 200 { "connectionId":..., "availableTransports":[...] }
HubConnection hub = HubConnectionBuilder.create("https://app/hub")
    .withAccessTokenProvider(...)
    .build();
hub.start();
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: confirm negotiate returns JSON before starting the hub.
Response r = httpClient.post(url + "/negotiate", authHeaders);
if (r.contentType() == null || !r.contentType().contains("json")) {
  throw new IllegalStateException(
    "Negotiate did not return JSON; status=" + r.statusCode()
    + " bodyPrefix=" + r.body().substring(0, Math.min(80, r.body().length())));
}

Try / catch

hub.onClosed(ex -> {
  if (ex != null && ex.getMessage().contains("Error reading NegotiateResponse")) {
    // likely HTML from proxy/auth; log negotiate response, fix infra, retry
  }
});

Prevention

When it happens

Trigger: The /negotiate endpoint returns non-JSON (HTML error page, empty body, gateway 502); truncated response due to connection drop; charset/encoding mismatch; the negotiate endpoint is hosted behind a proxy that rewrote the body.

Common situations: Reverse proxy (nginx/HAProxy) returning an HTML 502/503 instead of JSON; Azure SignalR negotiate going to a stale backend; server returning a redirect HTML page; auth layer returning a login HTML page because the bearer token was rejected.

Related errors


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

Appendix: source

Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/NegotiateResponse.java:86

                                this.availableTransports.add(transport);
                            }
                            reader.endObject();
                        }
                        reader.endArray();
                        break;
                    case "connectionId":
                        this.connectionId = reader.nextString();
                        break;
                    default:
                        // Skip unknown property, allows new clients to still work with old protocols
                        reader.skipValue();
                        break;
                }
            } while (reader.hasNext());
            reader.endObject();
            reader.close();
        } catch (IOException ex) {
            throw new RuntimeException("Error reading NegotiateResponse", ex);
        }
    }

    public NegotiateResponse(String url) {
        this.finalUrl = url;
    }

    public String getConnectionId() {
        return connectionId;
    }

    public Set<String> getAvailableTransports() {
        return availableTransports;
    }

    public String getRedirectUrl() {
        return redirectUrl;
    }

View on GitHub (pinned to 3600ca084e)