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
- curl -i the /negotiate URL with the same auth headers and inspect the raw body and status code.
- Ensure the hub endpoint is reachable and returns application/json; fix reverse proxy to pass through negotiate responses.
- 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
- Add a /negotiate health check to your deployment smoke test.
- Verify auth tokens are accepted before relying on negotiate.
- Ensure reverse proxies pass through JSON negotiate bodies unchanged.
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
- Error reading JSON.
- Negotiate redirection limit exceeded.
- ${negotiateResponse.getError()}
- There were no compatible transports on the server.
- Unexpected status code returned from negotiate
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)