SignalR/SignalR · error · HttpClientException
{0}
Error message
{0} What it means
Thrown inside the .Then continuation of DefaultHttpClient.Get when the HttpResponseMessage has a non-success status code (anything outside 2xx). The message is responseMessage.ToString() (status code + reason phrase). The HttpClientException.Response property is set, but the message and its RequestMessage are already disposed, so only property getters like StatusCode/ReasonPhrase are safe; reading the body will raise ObjectDisposedException. This is how SignalR surfaces server-side negotiate/poll failures to the caller.
Source
Thrown at src/Microsoft.AspNet.SignalR.Client/Http/DefaultHttpClient.cs:95
var httpClient = GetHttpClient(isLongRunning);
return httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token)
.Then(responseMessage =>
{
if (responseMessage.IsSuccessStatusCode)
{
responseDisposer.Set(responseMessage);
}
else
{
// Dispose the response (https://github.com/SignalR/SignalR/issues/4092)
responseMessage.RequestMessage.Dispose();
responseMessage.Dispose();
// None of the getters on HttpResponseMessage throw ODE, so it should be safe to give the catcher of the exception
// access to the response. They may get an ODE if they try to read the body, but that's OK.
throw new HttpClientException(responseMessage);
}
return (IResponse)new HttpResponseMessageWrapper(responseMessage);
});
}
/// <summary>
/// Makes an asynchronous http POST request to the specified url.
/// </summary>
/// <param name="url">The url to send the request to.</param>
/// <param name="prepareRequest">A callback that initializes the request with default values.</param>
/// <param name="postData">form url encoded data.</param>
/// <param name="isLongRunning">Indicates whether the request is long running</param>
/// <returns>A <see cref="T:Task{IResponse}"/>.</returns>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Handler cannot be disposed before response is disposed")]
public Task<IResponse> Post(string url, Action<IRequest> prepareRequest, IDictionary<string, string> postData, bool isLongRunning)
{
if (prepareRequest == null)View on GitHub (pinned to 693053b89a)
Solutions
- Catch HttpClientException and inspect ex.Response.StatusCode (do NOT try to read Response.Content — it is disposed).
- Verify the connection URL and that the server maps the hub at that path; append /negotiate only where the transport expects it.
- Ensure auth headers/credentials are set on the connection or in prepareRequest before the request flies.
- For 5xx, check server-side logs for the actual fault; retry with backoff only for transient 5xx/408.
Example fix
// before
var resp = await client.Get(url, prepare, isLongRunning);
// after
try { var resp = await client.Get(url, prepare, isLongRunning); }
catch (HttpClientException ex)
{
var code = ex.Response != null ? ex.Response.StatusCode : (HttpStatusCode)0;
// do NOT call ex.Response.Content.ReadAsStringAsync() here
} Defensive patterns
Strategy: try-catch
Type guard
static bool IsTransientFailure(HttpStatusCode c) =>
c == HttpStatusCode.RequestTimeout || c == HttpStatusCode.ServiceUnavailable ||
c == HttpStatusCode.BadGateway || c == HttpStatusCode.GatewayTimeout ||
(int)c >= 500; Try / catch
try
{
var resp = await client.Get(url, prepare, isLongRunning);
}
catch (HttpClientException ex)
{
var code = ex.Response != null ? ex.Response.StatusCode : (HttpStatusCode)0;
// never call ex.Response.Content.ReadAsStringAsync() — it is disposed
if (IsTransientFailure(code)) { /* schedule reconnect */ }
} Prevention
- Always wrap negotiate/poll GET in try/catch for HttpClientException.
- Read only status/header getters from ex.Response; the body and RequestMessage are disposed.
- Validate the endpoint URL and auth token before the request flies.
- Classify 5xx/408/503 as transient and reconnect with backoff; treat 4xx as fatal config errors.
When it happens
Trigger: The SignalR GET (typically /negotiate or a poll) returns 401/403/404/500/502/503; the endpoint URL is wrong or not mapped; an auth bearer token is rejected; the gateway returns an error page.
Common situations: Hub endpoint URL misspelled or missing the /negotiate suffix; expired/missing bearer token or credentials; reverse proxy/CORS returning an error; server process down (502/503); SignalR not mapped at the expected path (MapSignalR/MapHub).
Related errors
- Error message received from the server: '{0}'.
- The connection was stopped before it could be started.
- Negotiate redirection limit exceeded.
- You are using a version of the client that isn't compatible
- prepareRequest
AI-assisted analysis of SignalR/SignalR@693053b89a (2026-08-13).
Data as JSON: /api/errors/9270466cadd1fc22.
Report an issue: GitHub.