-
Notifications
You must be signed in to change notification settings - Fork 524
Conversation
halter73
commented
Jun 1, 2017
- Add IHttpMaxRequestBodySizeFeature
- Improve TestConnection.Receive timeout errors
@@ -20,6 +20,9 @@ public class KestrelServerLimits | |||
// Matches the default large_client_header_buffers in nginx. | |||
private int _maxRequestHeadersTotalSize = 32 * 1024; | |||
|
|||
// Disabled by default. | |||
private long? _maxRequestBodySize = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good default 😉
/// <summary> | ||
/// | ||
/// </summary> | ||
public interface IHttpMaxRequestBodySizeFeature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to move to HttpAbstractions, it's not Kestrel specific.
{ | ||
if (_contentLength > _context.MaxRequestBodySize) | ||
{ | ||
_context.RejectRequest(RequestRejectionReason.RequestBodyTooLarge); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is really misleading, it just throws an exception, it doesn't change any state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is typical kestrel code though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been misled by this too. At one point, someone compared this to explicit throws (e.g. throw MethodThatCreatesException()
) and concluded that it doesn't inline as well, enough that it hurts perf. I think that was @pakrym or @benaadams
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was tempted to change the name of RejectRequest to ThrowBadHttpRequestException as part of this change. I could still do that if we're in agreement that it makes this and all the other calling code at least slightly more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renaming to ThrowXxx
probably makes sense to indicate control is not returned to caller.
if (!_context.HasStartedConsumingRequestBody) | ||
{ | ||
_context.HasStartedConsumingRequestBody = true; | ||
OnReadStart(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens on the second call to ReadAsync if the first one throws here and is caught by the app? You need to reverse these lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Start was cleaner tbh. I understand why it was removed but it meant we avoid this lazy initialize once logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I definitely understand avoiding lazy initialization is cleaner. It's not even too inefficient to greedily start the pump now that the "max" size of the body pipe is 1 byte.
The problem is that the lazy init logic was required for the request feature to work nicely with Content-Length bodies, and if you are going to do some check only on the first read anyway, it's no longer cleaner to greedily start the pump.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't look like this got fixed.
{ | ||
if (ch1 == ';') | ||
{ | ||
consumed = reader.Cursor; | ||
examined = reader.Cursor; | ||
|
||
AddAndCheckConsumedBytes(reader.ConsumedBytes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Counting the framing is going to cause strange inconsistencies between Chunked, Content-Length, and HttpSysServer. @shirhatti I assume IIS does not count the chunked framing because it doesn't even see it, Http.Sys removes it, right?
do | ||
{ | ||
ReadCursor extensionCursor; | ||
if (ReadCursorOperations.Seek(buffer.Start, buffer.End, out extensionCursor, ByteCR) == -1) | ||
{ | ||
// End marker not found yet | ||
CheckExaminedBytes(buffer.Length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the wrong limit to apply to extensions. Let's review offline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's super trivial to change this to just count the "data" bytes if that's what we decide. I decided to do the more difficult thing first.
I'm not too concerned about breaking any apps by counting extension bytes towards the request body size limit since chunk extensions are so rare. Counting the framing bytes might make Kestrel behave differently that HttpSysServer, but the framing can easily take up to ~92% of the uploaded bytes without any chunk extensions or trailing headers. We can discuss offline how much we care about that.
input.Add("80000000\r\n"); | ||
|
||
var buffer = new byte[1024]; | ||
var ex = await Assert.ThrowsAsync<OverflowException>(async () => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be wrapped in an IOException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a test I added for existing behavior, so technically that's a breaking change but not really in practice.
"", | ||
""); | ||
await connection.ReceiveForcedEnd( | ||
"HTTP/1.1 413 Payload Too Large", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BadHttpRequestException is catchable by the app, correct? You only get this 413 if they do not catch it and it bubbles back out from the app pipeline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly correct. All future reads would also fail, so even if the app swallowed all exceptions and didn't write to the response body, the request draining code would see the exception which would still trigger a 413. If the app writes to the response body before the request is drained (or it's a non-keepalive request that doesn't get drained), then there might not be a 413 response.
Have déjà vu 😄 Good to standardize it as a common cross server feature though |
MaximumSizeLow = ServiceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0 | ||
WriterScheduler = InlineScheduler.Default, | ||
MaximumSizeHigh = 1, | ||
MaximumSizeLow = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, can you explain the change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prevents double buffering. Since the request body is only exposed directly as a Stream, it's always greedily consumed (i.e. it's impossible to inspect data from the request body via a Stream without consuming the data from the pipe). This means there's no functional reason to buffer here.
There could theoretically be a perf impact which is why I posted the CopyToAsync benchmark results. This is still pretty efficient since backpressure doesn't get applied to the client until the connection's Input pipe buffers up to MaxRequestBufferSize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory, this should really be set to MaxRequestBufferSize
and this limit should be decided by the transport. Basically our limits for the server shouldn't apply here. The problem is that the request is split for us between start line and headers and the body size...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidfowl Are you saying that ideally the request body Pipe would buffer instead of the connection "Input" Pipe?
{ | ||
_context.HasStartedConsumingRequestBody = true; | ||
OnReadStart(); | ||
var ignore = PumpAsync(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observe exceptions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PumpAsync is wrapped in a giant try/catch which will Complete the body pipe writer with the error, so the app can observe the exception. I'll add a comment though.
<value>The maximum request body size cannot be modified after the app has already started reading from the request body.</value> | ||
</data> | ||
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve"> | ||
<value>The maximum request body size cannot be modified after the request has be upgraded.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...."has been upgraded".
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests); | ||
} | ||
|
||
MaxRequestBodySize = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First validate the value is >= 0
@@ -499,13 +528,14 @@ private void ParseChunkedPrefix(ReadableBuffer buffer, out ReadCursor consumed, | |||
var chunkSize = CalculateChunkSize(ch1, 0); | |||
ch1 = ch2; | |||
|
|||
do | |||
while (reader.ConsumedBytes < 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you give this magic number a name by setting it as a const?
get => _maxRequestBodySize; | ||
set | ||
{ | ||
if (value <= 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quirky, but technically 0 is a valid limit, right?
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http | ||
{ | ||
/// <summary> | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: add a description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got lazy since I knew this would be moved to HttpAbstractions 😄
// TODO: Explain how to override | ||
/// <summary> | ||
/// Gets or sets the maximum allowed size of the current request body in bytes. | ||
/// When set to null, the maximunm request body size is unlimited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: maximum
{ | ||
if (_contentLength > _context.MaxRequestBodySize) | ||
{ | ||
_context.RejectRequest(RequestRejectionReason.RequestBodyTooLarge); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been misled by this too. At one point, someone compared this to explicit throws (e.g. throw MethodThatCreatesException()
) and concluded that it doesn't inline as well, enough that it hurts perf. I think that was @pakrym or @benaadams
86affb8
to
698ac44
Compare
/// <remarks> | ||
/// Defaults to null (unlimited). | ||
/// </remarks> | ||
public long? MaxRequestBodySize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add tests for default value, valid values and invalid values in KestrelServerLimitsTests
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -202,6 +203,24 @@ string IHttpRequestIdentifierFeature.TraceIdentifier | |||
set => TraceIdentifier = value; | |||
} | |||
|
|||
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add tests in FrameTests
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
698ac44
to
a094446
Compare
- Add IHttpMaxRequestBodySizeFeature - Improve TestConnection.Receive timeout errors
a094446
to
16757b0
Compare
⬆️ 📅 |
@@ -20,6 +21,10 @@ public class KestrelServerLimits | |||
// Matches the default large_client_header_buffers in nginx. | |||
private int _maxRequestHeadersTotalSize = 32 * 1024; | |||
|
|||
// Matches the default maxAllowedContentLength in IIS (~28.6 MB) | |||
// https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005 | |||
private long? _maxRequestBodySize = 30000000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided on 16mb in the meeting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember deciding the highest default of IIS/nginx which would be this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, go read the meeting notes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it in the meeting notes, but there's no reasoning. The decision was to use the larger default which is ~28.6 MB. I think that was written before we looked up what the largest default was. I can change it in the notes if you think that helps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, clear it with Damian and Barry, don't just change it.
|
get => MaxRequestBodySize; | ||
set | ||
{ | ||
if (HasStartedConsumingRequestBody) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does the application check this to make sure it's safe to do to avoid throwing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, the app has to keep track of whether or not it's read from the request body itself. I know this might break down a little if you are writing some reusable component where you want to change the upload limit, but only if nothing has been uploaded yet. That seems pretty niche though, and nothing uses this feature today ofc.
Do you want to add HasStartedConsumingRequestBody to some request feature? Do you want to do the same for WasUpgraded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the problem is you can add middleware anywhere in the pipeline that reads the body. There needs to be a way to not throw here.
Do you want to add HasStartedConsumingRequestBody to some request feature? Do you want to do the same for WasUpgraded?
Yes, or something more generally named like our other bools of this nature (HasResponseStarted etc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could add it to the new IHttpMaxRequestBodySizeFeature feature: LimitCanBeChanged or IsMutable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of those names are terrible but I agree with the sentiment.
{ | ||
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead); | ||
} | ||
if (_wasUpgraded) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
{ | ||
OnReadStart(); | ||
_context.HasStartedConsumingRequestBody = true; | ||
var ignore = PumpAsync(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _ = PumpAsync()
863ef2f
to
3dbfb00
Compare
/// Gets or sets the maximum allowed size of any request body in bytes. | ||
/// When set to null, the maximum request body size is unlimited. | ||
/// This limit has no affect on upgraded connections which are always unlimited. | ||
/// This can be overridden by <see cref="IHttpMaxRequestBodySizeFeature"/>. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would change this to "This can be overridden per-request via <see cref="IHttpMaxRequestBodySizeFeature"/>."
@@ -134,6 +139,28 @@ public int MaxRequestHeaderCount | |||
} | |||
|
|||
/// <summary> | |||
/// Gets or sets the maximum allowed size of any request body in bytes. | |||
/// When set to null, the maximum request body size is unlimited. | |||
/// This limit has no affect on upgraded connections which are always unlimited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/affect/effect