Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET handling requests when connection is dropped after switching to IIS8

I have an ASP.NET 3.5 app using WebForms, currently it is being hosted on IIS6. Everything behaves great.

However, after switching to a Windows 2012 server with IIS8 installed, we intermittently get truncated requests. The majority of the time this manifests in a viewstate exception in our event log, however, on forms that do not have ViewState, we get incomplete posts (the last few fields are missing / partially truncated).

This became so problematic that we escalated to Microsoft support, and after weeks of debugging, they said that this is the "correct" behavior for II7 and above. Their explanation was the change in the IIS pipeline from 6 to 7.

IIS6 and below would buffer the entire request before passing it along to Asp.net, truncated requests would be ignored.
IIS7 and above would send the request to Asp.net after the initial headers were sent, it would be up to the app to handle truncated requests.

This becomes problematic when either there are connectivity issues (the user unplugs their cable during tranmission) or when the user presses stop / reloads the page during a post.

In our HTTP logs, we see "connection_dropped" messages that correlate to the truncated requests.

I am having trouble believing that this behavior is intended, but we have tested on a few different servers and get the same results with IIS7 and above (Windows 2008, 2008 R2, and 2012).

My questions are:

1) Does this behavior even make sense?

2) If this is "correct" behavior, how do you protect your app against potentially processing incomplete data?

3) Why is it the application developer's responsibility to detect incomplete requests? Hypothetically, why would the app developer handle the incomplete request other than ignoring it?

Update

I wrote a small asp.net application and website to demonstrate the issue.

Server

Handler.ashx.cs

public class Handler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        if (context.Request.HttpMethod == "POST")
        {
            var lengthString = context.Request.Form["Length"];
            var data = context.Request.Form["Data"];

            if (lengthString == null)
            {
                throw new Exception("Missing field: Length");
            }

            if (data == null)
            {
                throw new Exception("Missing field: Data");
            }

            var expectedLength = int.Parse(lengthString);

            if (data.Length != expectedLength)
            {
                throw new Exception(string.Format("Length expected: {0}, actual: {1}, difference: {2}", expectedLength, data.Length, expectedLength - data.Length));
            }
        }

        context.Response.ContentType = "text/plain";
        context.Response.Write("Hello World, Request.HttpMethod=" + context.Request.HttpMethod);
    }

    public bool IsReusable
    {
        get { return false; }
    }
}

Client

Program.cs

static void Main(string[] args)
{
    var uri = new Uri("http://localhost/TestSite/Handler.ashx");
    var data = new string('a', 1024*1024); // 1mb

    var payload = Encoding.UTF8.GetBytes(string.Format("Length={0}&Data={1}", data.length, data));

    // send request truncated by 256 bytes
    // my assumption here is that the Handler.ashx should not try and handle such a request
    Post(uri, payload, 256);
}

private static void Post(Uri uri, byte[] payload, int bytesToTruncate)
{
    var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    {
        // this allows us to disconnect unexpectedly
        LingerState = new LingerOption(true, 0)
    };

    socket.Connect(uri.Host, uri.Port);

    SendRequest(socket, uri, payload, bytesToTruncate);

    socket.Close();
}

private static void SendRequest(Socket socket, Uri uri, byte[] payload, int bytesToTruncate)
{
    var headers = CreateHeaders(uri, payload.Length);

    SendHeaders(socket, headers);

    SendBody(socket, payload, Math.Max(payload.Length - bytesToTruncate, 0));
}

private static string CreateHeaders(Uri uri, int contentLength)
{
    var headers = new StringBuilder();

    headers.AppendLine(string.Format("POST {0} HTTP/1.1", uri.PathAndQuery));
    headers.AppendLine(string.Format("Host: {0}", uri.Host));
    headers.AppendLine("Content-Type: application/x-www-form-urlencoded");
    headers.AppendLine("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:26.0) Gecko/20100101 Firefox/99.0");
    headers.AppendLine("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
    headers.AppendLine("Connection: Close");
    headers.AppendLine(string.Format("Content-Length: {0}", contentLength));

    return headers.ToString();
}

private static void SendHeaders(Socket socket, string headers)
{
    socket.Send(Encoding.ASCII.GetBytes(headers));
    socket.Send(Encoding.ASCII.GetBytes("\n"));
}

private static void SendBody(Socket socket, byte[] payload, int numBytesToSend)
{
    socket.Send(payload, 0, numBytesToSend, SocketFlags.None);
}
like image 800
Matthew Avatar asked Mar 04 '14 04:03

Matthew


1 Answers

1) If you're running pipeline for the app pool to which your 3.5 application is assigned in Integrated mode, you might have trouble with how your requests are handled due to ISAPI behavior. You may be generating requests it doesn't understand properly and it then truncates them to a default value. Have you tried running the app pool in Classic mode?

2) Functional testing. Lots and lots of functional testing. Create a test harness and make all the calls your application can make to make sure it's working properly. This is not a 100% solution, but nothing really is. There are many computer science papers explaining why it's impossible to test every single possible situation in which your app may run based on the Halting Problem.

3) Because you wrote the code. You should not have incomplete requests because the request might be for an important piece of data and you need to send back an error saying that there was a problem processing a request, otherwise the issuing party just sees the request as having mysteriously vanished.

like image 141
gfish3000 Avatar answered Sep 23 '22 22:09

gfish3000