31

I am using the HttpClient class in .NET Framework 4.5.2.

I calling PostAsync against a third party web service. 80% of the time this post works, 20% of the time our response is cut short. In this situation we get the following exception:

System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host at System.Net.Sockets.NetworkStream.BeginRead(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state) --- End of inner exception stack trace --- at System.Net.Sockets.NetworkStream.BeginRead(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state) at System.Net.FixedSizeReader.StartReading() at System.Net.Security._SslStream.StartFrameHeader(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest) at System.Net.Security._SslStream.StartReading(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest) at System.Net.Security._SslStream.ProcessRead(Byte[] buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest) at System.Net.TlsStream.BeginRead(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback asyncCallback, Object asyncState) at System.Net.ConnectStream.BeginReadWithoutValidation(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state) at System.Net.ConnectStream.BeginRead(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state) at System.Net.Http.HttpClientHandler.WebExceptionWrapperStream.BeginRead(Byte[] buffer, Int32 offset, Int32 count, AsyncCallback callback, Object state) at System.Net.Http.StreamToStreamCopy.StartRead()

A subsequent identical request succeeds.

We cannot retry this request as the business action has already been taken. So it leaves us in an awkward situation.

This is my code:

using (var httpClient = new HttpClient())
{
    httpClient.DefaultRequestHeaders.Authorization = authorizationHeader;
    HttpContent httpContent = new StringContent(someXml);

    //Exception occurs on next line...
    var response = await httpClient.PostAsync("https://thirdpartyendpoint", httpContent);
    var responseXml = await response.Content.ReadAsStringAsync();  
    //convert to Dto              
}

The third-party service are successfully saving the record to their database and do not see any obvious exceptions at their end. They did note that the failing requests generally took longer (around 18-30 seconds) to write to the database than the successful requests.

What can I do to handle this better?

chwarr
  • 6,777
  • 1
  • 30
  • 57
jonho
  • 1,680
  • 2
  • 19
  • 29

3 Answers3

23

we resolved this problem with 2 code changes:

  1. Dispose of the httpResponseMessage and just work with a simple DTO

    using (var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage))
    {
        return await CreateDto(httpResponseMessage);
    }
    
  2. Downgrade the version of HTTP to v1.0

    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url))
    {
        Version = HttpVersion.Version10,
        Content = httpContent
    };
    
    await client.SendAsync(httpRequestMessage);
    

which has the effect of adding this Http header

Connection: close 

rather than this

Connection: keep-alive
spottedmahn
  • 14,823
  • 13
  • 108
  • 178
jonho
  • 1,680
  • 2
  • 19
  • 29
  • Any ideas about how to apply HttpVersion to httpClient get requests? – garenyondem Mar 07 '16 at 13:38
  • 2
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(url)) { Version = HttpVersion.Version10, Content = httpContent }; await client.SendAsync(httpRequestMessage); should do it – jonho Mar 14 '16 at 22:18
  • Using DTOs instead solved my problem. I suspected it would help. Thank you! – zinczinc May 20 '18 at 23:06
  • 1
    just by changing DefaultRequestHeaders.ConnectionClose to true worked for me :) – juanora May 29 '18 at 13:30
  • Downgrade to HTTP v1.0 worked for me. I found this is needed when post to Windows Server 2008 R2 but not Windows Server 2012, Any idea why? – Robert Nov 29 '18 at 03:09
  • I was using a long lived static HttpClient. You're comment about Connection: close and keep-alive was what I needed to see what was going wrong. – Mark Lauter Mar 16 '19 at 01:30
  • But this might create performance side-effects, since you cannot use persistent connections. See here: https://stackoverflow.com/a/247026/2173353. – user2173353 Jun 27 '19 at 14:43
5

I had a similar problem with the use of a shared HttpClient connecting to a server for REST calls. The problem ended up being a mismatch between the KeepAlive timeout on the client and server. The client side timeout is set by the MaxServicePointIdleTime setting on the ServicePointManager and defaults to 100s. The server side idle timeout was set to a shorter value in our server.

Having a shorter timeout on the server as compared to the client resulted in the server sporadically closing a connection just when the client was attempting to connect. This resulted in the reported exception.

Note that I ultimately found the problem because I also received this exception under the same conditions:

System.Net.WebException: The underlying connection was closed: A connection that was expected to be kept alive was closed by the server.
Steve Wranovsky
  • 5,503
  • 4
  • 34
  • 52
  • This sounds interesting. Can you say what you did to set the server idle timeout? For the client I guess you used `ServicePointManager.FindServicePoint(...)`. – user2173353 Jun 28 '19 at 14:38
  • 1
    @user2173353, we had a customer HttpServer that we could set the KeepAlive directly on a socket. I'm not sure how to set it in other cases. – Steve Wranovsky Jul 18 '19 at 18:32
3

I had the same error (Error while copying content to a stream) with HTTPClient PutAsync() method:

using (StreamContent content = new StreamContent(stream))
{
    HttpResponseMessage response = await client.PutAsync(url, content))
}

You need to specify the HttpCompletionOption.ResponseHeadersRead flag which is not available in PutAsync so I switched to SendAsync:

using (StreamContent content = new StreamContent(stream))
{
    var httpRequest = new HttpRequestMessage(HttpMethod.Put, url);
    httpRequest.Content = content;

    HttpResponseMessage response = await client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
}
DIMMACK
  • 189
  • 6
  • Any idea why this is needed? Based on the description in the docs, I don't understand why this would help: https://learn.microsoft.com/en-us/uwp/api/Windows.Web.Http.HttpCompletionOption. – user2173353 Jun 27 '19 at 14:47
  • 1
    Maybe the answer is here: https://github.com/dotnet/corefx/issues/24683#issuecomment-435189110 – Wietze Jul 02 '19 at 12:08