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

  1. Catch HttpClientException and inspect ex.Response.StatusCode (do NOT try to read Response.Content — it is disposed).
  2. Verify the connection URL and that the server maps the hub at that path; append /negotiate only where the transport expects it.
  3. Ensure auth headers/credentials are set on the connection or in prepareRequest before the request flies.
  4. 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

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


AI-assisted analysis of SignalR/SignalR@693053b89a (2026-08-13). Data as JSON: /api/errors/9270466cadd1fc22. Report an issue: GitHub.