Send Resets for unhandled app exceptions #2733

This commit is contained in:
Chris Ross (ASP.NET) 2018-08-02 16:04:21 -07:00
parent 7e073cb5f6
commit ea7e53cab0
5 changed files with 263 additions and 5 deletions

View File

@ -563,4 +563,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2StreamErrorMoreDataThanLength" xml:space="preserve">
<value>More data received than specified in the Content-Length header.</value>
</data>
<data name="Http2StreamErrorAfterHeaders" xml:space="preserve">
<value>An error occured after the response headers were sent, a reset is being sent.</value>
</data>
</root>

View File

@ -1032,8 +1032,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (HasResponseStarted)
{
// We can no longer change the response, so we simply close the connection.
_keepAlive = false;
ErrorAfterResponseStarted();
return Task.CompletedTask;
}
@ -1058,6 +1057,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return WriteSuffix();
}
protected virtual void ErrorAfterResponseStarted()
{
// We can no longer change the response, so we simply close the connection.
_keepAlive = false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task ProduceEndAwaited()
{

View File

@ -362,10 +362,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
AbortCore(abortReason);
}
protected override void ErrorAfterResponseStarted()
{
// We can no longer change the response, send a Reset instead.
base.ErrorAfterResponseStarted();
var abortReason = new ConnectionAbortedException(CoreStrings.Http2StreamErrorAfterHeaders);
ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR);
}
protected override void ApplicationAbort()
{
var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication);
ResetAndAbort(abortReason, Http2ErrorCode.CANCEL);
ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR);
}
private void ResetAndAbort(ConnectionAbortedException abortReason, Http2ErrorCode error)

View File

@ -2086,6 +2086,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamErrorMoreDataThanLength()
=> GetString("Http2StreamErrorMoreDataThanLength");
/// <summary>
/// An error occured after the response headers were sent, a reset is being sent.
/// </summary>
internal static string Http2StreamErrorAfterHeaders
{
get => GetString("Http2StreamErrorAfterHeaders");
}
/// <summary>
/// An error occured after the response headers were sent, a reset is being sent.
/// </summary>
internal static string FormatHttp2StreamErrorAfterHeaders()
=> GetString("Http2StreamErrorAfterHeaders");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -1063,6 +1064,233 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task ContentLength_Response_FirstWriteMoreBytesWritten_Throws_Sends500()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(async context =>
{
context.Response.ContentLength = 11;
await context.Response.WriteAsync("hello, world"); // 12
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
Assert.Contains(_logger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too many bytes written (12 of 11).") ?? false);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("500", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task ContentLength_Response_MoreBytesWritten_ThrowsAndResetsStream()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(async context =>
{
context.Response.ContentLength = 11;
await context.Response.WriteAsync("hello,");
await context.Response.WriteAsync(" world");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 56,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 6,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "Response Content-Length mismatch: too many bytes written (12 of 11).");
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("11", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task ContentLength_Response_NoBytesWritten_Sends500()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(context =>
{
context.Response.ContentLength = 11;
return Task.CompletedTask;
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
Assert.Contains(_logger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too few bytes written (0 of 11).") ?? false);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("500", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task ContentLength_Response_TooFewBytesWritten_Resets()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(context =>
{
context.Response.ContentLength = 11;
return context.Response.WriteAsync("hello,");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 56,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 6,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "Response Content-Length mismatch: too few bytes written (6 of 11).");
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("11", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task ApplicationExeption_BeforeFirstWrite_Sends500()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(context =>
{
throw new Exception("App Faulted");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
Assert.Contains(_logger.Messages, m => (m.Exception?.Message.Contains("App Faulted") ?? false) && m.LogLevel == LogLevel.Error);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("500", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task ApplicationExeption_AfterFirstWrite_Resets()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(async context =>
{
await context.Response.WriteAsync("hello,");
throw new Exception("App Faulted");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 6,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "App Faulted");
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
Assert.Equal(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
}
[Fact]
public async Task RST_STREAM_Received_AbortsStream()
{
@ -1221,7 +1449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
});
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, CoreStrings.ConnectionAbortedByApplication);
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.ConnectionAbortedByApplication);
await WaitForAllStreamsAsync();
Assert.Contains(1, _abortedStreamIds);
@ -1273,7 +1501,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: 0,
withStreamId: 1);
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, CoreStrings.ConnectionAbortedByApplication);
await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.ConnectionAbortedByApplication);
await WaitForAllStreamsAsync();
Assert.Contains(1, _abortedStreamIds);