{"record":{"id":"9270466cadd1fc22","repo":"SignalR/SignalR","slug":"0","errorCode":null,"errorMessage":"{0}","messagePattern":"\\{0\\}","errorType":"http","errorClass":"HttpClientException","httpStatus":null,"severity":"error","filePath":"src/Microsoft.AspNet.SignalR.Client/Http/DefaultHttpClient.cs","lineNumber":95,"sourceCode":"\n            var httpClient = GetHttpClient(isLongRunning);\n\n            return httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token)\n                 .Then(responseMessage =>\n                 {\n                     if (responseMessage.IsSuccessStatusCode)\n                     {\n                         responseDisposer.Set(responseMessage);\n                     }\n                     else\n                     {\n                         // Dispose the response (https://github.com/SignalR/SignalR/issues/4092)\n                         responseMessage.RequestMessage.Dispose();\n                         responseMessage.Dispose();\n\n                         // None of the getters on HttpResponseMessage throw ODE, so it should be safe to give the catcher of the exception\n                         // access to the response. They may get an ODE if they try to read the body, but that's OK.\n                         throw new HttpClientException(responseMessage);\n                     }\n\n                     return (IResponse)new HttpResponseMessageWrapper(responseMessage);\n                 });\n        }\n\n        /// <summary>\n        /// Makes an asynchronous http POST request to the specified url.\n        /// </summary>\n        /// <param name=\"url\">The url to send the request to.</param>\n        /// <param name=\"prepareRequest\">A callback that initializes the request with default values.</param>\n        /// <param name=\"postData\">form url encoded data.</param>\n        /// <param name=\"isLongRunning\">Indicates whether the request is long running</param>\n        /// <returns>A <see cref=\"T:Task{IResponse}\"/>.</returns>\n        [SuppressMessage(\"Microsoft.Reliability\", \"CA2000:Dispose objects before losing scope\", Justification = \"Handler cannot be disposed before response is disposed\")]\n        public Task<IResponse> Post(string url, Action<IRequest> prepareRequest, IDictionary<string, string> postData, bool isLongRunning)\n        {\n            if (prepareRequest == null)","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/SignalR/SignalR/blob/693053b89a9e1f5ce819e3233ed159a6409de22b/src/Microsoft.AspNet.SignalR.Client/Http/DefaultHttpClient.cs#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nvar resp = await client.Get(url, prepare, isLongRunning);\n// after\ntry { var resp = await client.Get(url, prepare, isLongRunning); }\ncatch (HttpClientException ex)\n{\n    var code = ex.Response != null ? ex.Response.StatusCode : (HttpStatusCode)0;\n    // do NOT call ex.Response.Content.ReadAsStringAsync() here\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"static bool IsTransientFailure(HttpStatusCode c) =>\n    c == HttpStatusCode.RequestTimeout || c == HttpStatusCode.ServiceUnavailable ||\n    c == HttpStatusCode.BadGateway || c == HttpStatusCode.GatewayTimeout ||\n    (int)c >= 500;","tryCatchPattern":"try\n{\n    var resp = await client.Get(url, prepare, isLongRunning);\n}\ncatch (HttpClientException ex)\n{\n    var code = ex.Response != null ? ex.Response.StatusCode : (HttpStatusCode)0;\n    // never call ex.Response.Content.ReadAsStringAsync() — it is disposed\n    if (IsTransientFailure(code)) { /* schedule reconnect */ }\n}","preventionTips":["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."],"tags":["signalr","http-client","server-error","status-code","negotiate","object-disposed"],"backgroundTag":null,"analyzedSha":"693053b89a9e1f5ce819e3233ed159a6409de22b","analyzedAt":"2026-08-13T22:23:59.793Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}