Gracefully handle exceptions thrown from OnStarting callbacks

- If OnStarting is being called after the app func has completed, return a 500.
- If Onstarting is being called due to a call to write, throw from write.
This commit is contained in:
Stephen Halter 2015-07-20 12:26:39 -07:00
parent 6a01043e1a
commit 4f0480a4d0
2 changed files with 99 additions and 7 deletions

View File

@ -25,7 +25,6 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
static Encoding _ascii = Encoding.ASCII;
Mode _mode;
private bool _resultStarted;
private bool _responseStarted;
private bool _keepAlive;
private readonly FrameRequestHeaders _requestHeaders = new FrameRequestHeaders();
@ -246,6 +245,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
try
{
await Application.Invoke(this).ConfigureAwait(false);
// Trigger FireOnStarting if ProduceStart hasn't been called yet.
// We call it here, so it can go through our normal error handling
// and respond with a 500 if an OnStarting callback throws.
if (!_responseStarted)
{
FireOnStarting();
}
}
catch (Exception ex)
{
@ -284,7 +291,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public void ProduceContinue()
{
if (_resultStarted) return;
if (_responseStarted) return;
string[] expect;
if (HttpVersion.Equals("HTTP/1.1") &&
@ -306,11 +313,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
public void ProduceStart(bool immediate = true)
{
if (_resultStarted) return;
_resultStarted = true;
// ProduceStart shouldn't no-op in the future just b/c FireOnStarting throws.
if (_responseStarted) return;
FireOnStarting();
_responseStarted = true;
var status = ReasonPhrases.ToStatus(StatusCode, ReasonPhrase);
@ -334,7 +339,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
if (ex != null)
{
if (_resultStarted)
if (_responseStarted)
{
// We can no longer respond with a 500, so we simply close the connection.
ConnectionControl.End(ProduceEndType.SocketDisconnect);

View File

@ -535,5 +535,92 @@ namespace Microsoft.AspNet.Server.KestrelTests
}
}
}
[Fact]
public async Task ThrowingInOnStartingResultsIn500Response()
{
using (var server = new TestServer(frame =>
{
frame.OnStarting(_ =>
{
throw new Exception();
}, null);
frame.ResponseHeaders.Clear();
frame.ResponseHeaders["Content-Length"] = new[] { "11" };
// If we write to the response stream, we will not get a 500.
return Task.FromResult<object>(null);
}))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"GET / HTTP/1.1",
"Connection: close",
"",
"");
await connection.Receive(
"HTTP/1.1 500 Internal Server Error",
"");
await connection.ReceiveStartsWith("Date:");
await connection.Receive(
"Content-Length: 0",
"Server: Kestrel",
"",
"HTTP/1.1 500 Internal Server Error",
"");
await connection.ReceiveStartsWith("Date:");
await connection.ReceiveEnd(
"Content-Length: 0",
"Server: Kestrel",
"Connection: close",
"",
"");
}
}
}
[Fact]
public async Task ThrowingInOnStartingResultsInFailedWrites()
{
using (var server = new TestServer(async frame =>
{
var onStartingException = new Exception();
frame.OnStarting(_ =>
{
throw onStartingException;
}, null);
frame.ResponseHeaders.Clear();
frame.ResponseHeaders["Content-Length"] = new[] { "11" };
var writeException = await Assert.ThrowsAsync<Exception>(async () =>
await frame.ResponseBody.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
Assert.Same(onStartingException, writeException);
// The second write should succeed since the OnStarting callback will not be called again
await frame.ResponseBody.WriteAsync(Encoding.ASCII.GetBytes("Exception!!"), 0, 11);
}))
{
using (var connection = new TestConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
"Content-Length: 11",
"",
"Exception!!"); ;
}
}
}
}
}