I have an ASP.NET 3.5 application using WebForms, it is currently hosted on IIS6. Everything behaves perfectly.
However, after switching to the Windows 2012 server with IIS8 installed, we periodically receive truncated requests. In most cases, this is manifested in the exception of viewstate in our event log, however, in forms that do not have ViewState, we receive incomplete messages (the last few fields are missing / partially truncated).
It became so problematic that we grew up to support Microsoft, and after several weeks of debugging, they said that this was the โcorrectโ behavior for II7 and higher. Their explanation was a change in the IIS pipeline from 6 to 7.
IIS6 and below will buffer the entire request before sending it to Asp.net, truncated requests will be ignored.
IIS7 and above would send an Asp.net request after sending the initial headers, this would be prior to the application for processing truncated requests.
This becomes problematic when there are connection problems (the user disconnects his cable during the transaction) or when the user presses the page stop / reload button during the message.
In our HTTP logs, we see "connection_dropped" messages that correlate with truncated requests.
I had problems believing that this behavior was intended, but we tested several servers and got the same results with IIS7 and higher (Windows 2008, 2008 R2 and 2012).
My questions:
1) Does this behavior make sense?
2) If this is the โcorrectโ behavior, how to protect the application from potential processing of incomplete data?
3) Why is the developer responsible for detecting incomplete requests? Hypothetically, why does the application developer handle an incomplete request other than ignoring it?
Update
I wrote a small asp.net application and website to demonstrate the problem.
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); }