Closed napiro closed 7 years ago
Hi,
I also ran into this problem. Here is what I got from research:
This error is an OS error (win32 code 64, ERROR_NETNAME_DELETED, https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx) that occurs during network communication when the client has dropped the connection but the server is still trying to send response (especially when the server response is large).
This is normal behavior and should be handled by the server code (i.e. abort response feed as it is no longer necessary).
Grapevine has a flaw in the exception handling of exceptions that are raised AFTER the response has been sent that prevents the handling of the above-mentioned exception - even when the exception is caught, Grapevine will fail in the following way:
in Router.Route(IHttpContext context, IList
in Router.Route(object state)
if (e is NotFoundException) { context.Response.SendResponse(HttpStatusCode.NotFound, e.Message); return; }
I don't see any workaround for that behavior, because even general exception handling by intercepting Application.ThreadException and AppDomain.CurrentDomain.UnhandledException is unable to catch the exception that occurs on Response.ContentLength64 = contents.Length;
As a temporary fix I will include the Grapevine project directly into my solution and will modify the code so that it won't treat the !context.WasRespondedTo as an exceptional condition. I hope that Mr. Scott Offen will implement a more meaningful fix in the next versions of the framework.
Thank you for Grapevine, Scott! Daniel
P.S. I believe the problem is here:
public bool WasRespondedTo => Response.ResponseSent;
Response.ResponseSent is not semantically equivalent to WasRespondedTo. WasRespondedTo must be set when at least one route has been found that can handle the request; Response.ResponseSent indicates that the entire response has been sent, which may not be the case if the client breaks the connection, and yet, the server has done everything correctly, and no exception must be thrown. I think, there are actually 3 different indicators during responding in Grapevine:
Here is a temporary fix:
in HttpResponse.cs
public void SendResponse(byte[] contents)
{
if (RequestHeaders.AllKeys.Contains("Accept-Encoding") && RequestHeaders["Accept-Encoding"].Contains("gzip") && contents.Length > 1024)
{
using (var ms = new MemoryStream())
{
using (var zip = new GZipStream(ms, CompressionMode.Compress))
{
zip.Write(contents, 0, contents.Length);
}
contents = ms.ToArray();
}
Response.Headers["Content-Encoding"] = "gzip";
}
try
{
Response.ContentLength64 = contents.Length;
Response.OutputStream.Write(contents, 0, contents.Length);
Response.OutputStream.Close();
Advanced.Close();
}
catch (HttpListenerException ex)
{
switch (ex.NativeErrorCode)
{
// ERROR_NETNAME_DELETED, https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
case 64:
Console.WriteLine("Connection aborted during SendResponse.");
break;
default:
Console.WriteLine(ex.ToString());
break;
}
}
}
in Router.cs
public void Route(IHttpContext context, IList<IRoute> routing)
{
if (routing == null || !routing.Any()) throw new RouteNotFoundException(context);
var totalRoutes = routing.Count;
var routeCounter = 0;
Logger.BeginRouting($"{context.Request.Id} - {context.Request.Name} has {totalRoutes} routes");
bool routeFound = false;
try
{
OnBeforeRouting(context);
foreach (var route in routing.Where(route => route.Enabled))
{
routeFound = true;
routeCounter++;
route.Invoke(context);
Logger.RouteInvoked($"{context.Request.Id} - {routeCounter}/{totalRoutes} {route.Name}");
if (ContinueRoutingAfterResponseSent) continue;
if (context.WasRespondedTo) break;
}
}
finally
{
OnAfterRouting(context);
Logger.EndRouting($"{context.Request.Id} - {routeCounter} of {totalRoutes} routes invoked");
}
if (!routeFound) throw new RouteNotFoundException(context);
}
I've implemented a fix to this, based in part by your suggested fix.
First, in the SendResponse
method, I wrapped writing to the response in a try/finally block:
try
{
Response.ContentLength64 = contents.Length;
Response.OutputStream.Write(contents, 0, contents.Length);
}
finally
{
Response.OutputStream.Close();
Advanced.Close();
}
This ensures that even if an exception was thrown, the HttpResponse.ResponseSent
property still gets set, which is what the HttpContext.WasRespondedTo
bool keys off of, and therefore prevent the RouteNotFoundException
from being called, which causes further exceptions to be thrown
It will also throw the exception back up to the router, so I added a catch block to the existing try/finally block that was already there:
catch (HttpListenerException e)
{
var msg = e.NativeErrorCode == 64
? "Connection aborted by client"
: "An error occured while attempting to respond to the request";
Logger.Error(msg, e);
}
This logs to exception with a message appropriate to the error that occurred, and the server continues running without problem.
Thoughs?
Response.ResponseSent is not semantically equivalent to WasRespondedTo. WasRespondedTo must be set when at least one route has been found that can handle the request; Response.ResponseSent indicates that the entire response has been sent, which may not be the case if the client breaks the connection, and yet, the server has done everything correctly, and no exception must be thrown.
This (emphasis mine) isn't entirely correct.
I anticipate that multiple routes may be found that should be invoked on the context, and iterate over them until one of them actually sends a response (default behavior, a flag can override this). Even if code is executed, unless at least one route attempts close the response, the request has not been responded to. This can be done in one of two ways:
Calls to SendResponse(byte[] contents)
will automatically close the response
Close the response w/o actually sending anything by calling HttpContext.HttpResponse.Advanced.Close()
It is the closing of the response that indicates whether the request was appropriately responded to.
Hi
Today I've encountered an exception which can be reproduced reliably with version 4.1.
After a second an exception is thrown in class HttpResponse:
The problem seems to be that the client closes the connection while the server tries to write data to the stream which fails. The following line is responsible for the exception:
Is this something you are aware of? Is this something you want to make transparent from the framework point of view and should be handled outside of Grapevine or is it a bug which should be fixed?
Currently I just ignore this error.
Thanks
BTW: I like this project very much!