Add a request body size limit#1877
Conversation
halter73
commented
Jun 1, 2017
- Add IHttpMaxRequestBodySizeFeature
- Improve TestConnection.Receive timeout errors
| private int _maxRequestHeadersTotalSize = 32 * 1024; | ||
|
|
||
| // Disabled by default. | ||
| private long? _maxRequestBodySize = null; |
| /// <summary> | ||
| /// | ||
| /// </summary> | ||
| public interface IHttpMaxRequestBodySizeFeature |
There was a problem hiding this comment.
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.
This method is really misleading, it just throws an exception, it doesn't change any state.
There was a problem hiding this comment.
This is typical kestrel code though.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
Doesn't look like this got fixed.
| consumed = reader.Cursor; | ||
| examined = reader.Cursor; | ||
|
|
||
| AddAndCheckConsumedBytes(reader.ConsumedBytes); |
There was a problem hiding this comment.
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?
| 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.
This is the wrong limit to apply to extensions. Let's review offline.
There was a problem hiding this comment.
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.
It should be wrapped in an IOException
There was a problem hiding this comment.
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.
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.
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.
+1, can you explain the change?
There was a problem hiding this comment.
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.
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.
@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.
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.
...."has been upgraded".
| throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests); | ||
| } | ||
|
|
||
| MaxRequestBodySize = value; |
There was a problem hiding this comment.
First validate the value is >= 0
| ch1 = ch2; | ||
|
|
||
| do | ||
| while (reader.ConsumedBytes < 10) |
There was a problem hiding this comment.
can you give this magic number a name by setting it as a const?
| { | ||
| get => _maxRequestBodySize; | ||
| set | ||
| { |
There was a problem hiding this comment.
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.
Nit: add a description
There was a problem hiding this comment.
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. |
| { | ||
| if (_contentLength > _context.MaxRequestBodySize) | ||
| { | ||
| _context.RejectRequest(RequestRejectionReason.RequestBodyTooLarge); |
There was a problem hiding this comment.
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
| /// </summary> | ||
| /// <remarks> | ||
| /// Defaults to null (unlimited). | ||
| /// </remarks> |
There was a problem hiding this comment.
Add tests for default value, valid values and invalid values in KestrelServerLimitsTests.
| set => TraceIdentifier = value; | ||
| } | ||
|
|
||
| long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize |
698ac44 to
a094446
Compare
- Add IHttpMaxRequestBodySizeFeature - Improve TestConnection.Receive timeout errors
a094446 to
16757b0
Compare
|
⬆️ 📅 |
|
|
||
| // 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.
We decided on 16mb in the meeting
There was a problem hiding this comment.
I remember deciding the highest default of IIS/nginx which would be this.
There was a problem hiding this comment.
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.
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.
How does the application check this to make sure it's safe to do to avoid throwing?
There was a problem hiding this comment.
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.
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.
Could add it to the new IHttpMaxRequestBodySizeFeature feature: LimitCanBeChanged or IsMutable
There was a problem hiding this comment.
Both of those names are terrible but I agree with the sentiment.
| { | ||
| throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead); | ||
| } | ||
| if (_wasUpgraded) |
| { | ||
| OnReadStart(); | ||
| _context.HasStartedConsumingRequestBody = true; | ||
| var ignore = PumpAsync(); |
863ef2f to
3dbfb00
Compare
| /// <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.
I would change this to "This can be overridden per-request via <see cref="IHttpMaxRequestBodySizeFeature"/>."
|
|
||
| /// <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. |