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);
}
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.
The reason IIS changed its behaviour because we (developers) needed more control over request handling. In case of broken request, we had problem investigating cause of invisible requests. We need to log requests at application level for investigation & record keeping. For example, if request involves a financial transaction like credit card transaction, we need more control and we need to record every step for compliance.
IIS is a web server framework and application level data validation is not their responsibility. If request was broken, that means the input was incomplete & application level logic will decide what to do. Application must respond correct error codes and by failures. This is the reason ASP.NET mvc has model validation which allows you to validate complete input at application level.
You can use IsClientConnected to check whether the underlying socket is still connected or not.
As the web has gone more AJAX, and more mobile, and we sometimes use ping to check health of remote services, we do not necessarily conclude that broken request is an error and must be dropped. We may still want to live with broken requests. It is the choice that application level developer can make and not the IIS.
链接地址: http://www.djcxy.com/p/77874.html