-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validate request size against global or endpoint provided max request size #28
Conversation
Codecov Report
@@ Coverage Diff @@
## master #28 +/- ##
===========================================
+ Coverage 89.79% 89.8% +0.01%
- Complexity 1823 1832 +9
===========================================
Files 137 137
Lines 5341 5357 +16
Branches 700 703 +3
===========================================
+ Hits 4796 4811 +15
Misses 368 368
- Partials 177 178 +1
Continue to review full report at Codecov.
|
@@ -21,10 +24,19 @@ | |||
* This handler should come after {@link io.netty.handler.codec.http.HttpRequestDecoder} and {@link | |||
* SmartHttpContentCompressor} in the pipeline. | |||
* | |||
* The request size is tracked and if it exceeds the configured global or a given endpoint's override, and exception |
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.
Minor typo - "and exception" instead of "an exception".
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 eyes. fixed.
|
||
private int getConfiguredMaxRequestSize(HttpProcessingState state) { | ||
int maxConfiguredRequestSize = maxRequestSizeInBytes; | ||
Optional<Integer> endpointMaxRequestSizeOverride = state.getEndpointForExecution().maxRequestSizeInBytesOverride(); |
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.
state.getEndpointForExecution()
can return null depending on the state of the request. A null endpoint should be considered the same as an empty optional, i.e. use the global default.
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.
Now that I think about it the endpoint could also return null. It would be a bad/broken endpoint but we still shouldn't NPE if someone does it. If null is returned from an endpoint's maxRequestSizeInBytesOverride()
it should also be treated as an empty optional (i.e. use global default).
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.
After some more thinking about all this, here are some general refactors that I think should be done:
- The max request size in bytes (MRSB) was originally an int because once upon a time we used Netty's
HttpObjectAggregator
, and that handler did the MRSB check and required an int. We no longer have that restriction, so we should use a long instead to get rid of the 2GB ceiling on MRSB. - Since we have to check for null on the endpoint override anyway even with
Optional
, and the core stuff tends to use nulls rather than optionals to avoid the perf hit of optionals, theEndpoint.maxRequestSizeInBytesOverride()
method signature should probably change to just return aLong
object. Returning null from the override method would mean "use the global default", and returning anything non-null would mean "use this value instead of the global default".
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.
thanks for the details. I'll update the logic where it handles the endpoint or the endpoint's override as null and will return the global default.
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.
Never mind on moving stuff to longs. As you pointed out, we're ultimately limited by the fact that byte[], ByteBuf, etc lengths are ints. So without some major refactoring to allow for exposing payloads as InputStreams or something similar we're stuck with an int limit.
* @author Nic Munroe | ||
*/ | ||
public class RequestInfoSetterHandler extends BaseInboundHandlerWithTracingAndMdcSupport { | ||
|
||
private final int maxRequestSizeInBytes; |
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.
Suggest renaming to defaultMaxRequestSizeInBytes
or something else that indicates it's the default/global.
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.
will do: globalConfiguredMaxRequestSizeInBytes ?
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.
Sure
.extract(); | ||
|
||
assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code()); | ||
assertThat(response.asString()).contains("Malformed request"); |
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.
Also look for the metadata here (and other places where we're verifying that the error was thrown).
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 call. i've added an assert for message and cause
.port(serverConfig.endpointsPort()) | ||
.basePath(BasicEndpoint.MATCHING_PATH) | ||
.log().all() | ||
.body("{'key1':'value1'}") |
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'd suggest that the global and endpoint-override max request size values be constants that can be referenced by these tests, and dynamically generate a request of the appropriate size in bytes relative to whatever constant you're testing against.
e.g. for this test:
.body(generatePayloadOfSizeInBytes(GLOBAL_MAX_REQUEST_SIZE + 1))
The dynamically generated payload doesn't have to be valid JSON - it can just be random characters, or all the same character, or whatever you want to do to make it easy - the only thing that matters is the size of the payload that's returned.
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 suggestion. I've incorporated this pattern in here.
|
||
// when | ||
try { | ||
handler.doChannelRead(ctxMock, httpContent); |
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.
AssertJ has a convenience method for all this:
Throwable thrownException = Assertions.catchThrowable(() -> handler.doChannelRead(ctxMock, httpContent));
Then you'd assert something on the exception which implicitly verifies the exception was thrown since if the assertion was executed on null it would also fail. The existing assertion in this test is sufficient, for example.
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.
thanks for the tip. I've converted them over.
@Test | ||
public void doChannelRead_allows_endpoints_to_override_max_request_size_setting_lower() { | ||
// given | ||
//handler is configured for 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.
Reference maxRequestSizeInBytes
in the tests so you don't have to put these comments here and also removes the potential for accidental test breakage or worse, accidental regression in that the tests don't catch.
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 call. I've moved this down in the tests so it is clearer the state of the test in the 'given'
/** | ||
* @return The max request size in bytes that you want to allow for this specific endpoint, or Optional.empty() | ||
* if you want to use the app-wide default max request size | ||
* returned by {@link ServerConfig#maxRequestSizeInBytes()} ()}. |
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.
Trailing ()}
that needs to be deleted. Also, the javadoc should include something about returning 0 to disable request size validation entirely.
3f741cf
to
e9ae5ea
Compare
private static String generatePayloadOfSizeInBytes(int length) { | ||
StringBuilder sb = new StringBuilder(length); | ||
for(int i = 0; i < length; i++) { | ||
sb.append(i); |
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.
For anything i >= 11 you'll be getting a string larger than the requested length in bytes. You could do sb.append(i % 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.
nice catch.
.port(serverConfig.endpointsPort()) | ||
.basePath(BasicEndpoint.MATCHING_PATH) | ||
.log().all() | ||
.body(generatePayloadOfSizeInBytes(GLOBAL_MAX_REQUEST_SIZE - 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.
Get rid of the - 1
I think?
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 call. its unnecessary. removed!
@@ -93,4 +119,102 @@ public void doChannelRead_checks_for_fully_send_responses_but_does_nothing_else_ | |||
assertThat(result).isEqualTo(PipelineContinuationBehavior.CONTINUE); | |||
} | |||
|
|||
@Test | |||
public void doChannelRead_returns_do_not_fire_when_null_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.
Thanks for adding these missing tests!
@@ -390,12 +390,11 @@ default int maxOpenIncomingServerChannels() { | |||
/** | |||
* @return The maximum allowed request size in bytes. If netty receives a request larger than this then it will | |||
* throw a {@link io.netty.handler.codec.TooLongFrameException}. | |||
* This value is an integer, so the max you can set it to is {@link Integer#MAX_VALUE}, which corresponds to 2^31-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.
I would leave the comment about max size in here (but since you're fixing the NOTE
that line should obviously go).
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 added it back.
@@ -6,6 +6,8 @@ | |||
import com.fasterxml.jackson.core.type.TypeReference; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
|
|||
import java.util.Optional; |
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.
Optional import no longer needed.
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 found the other, but missed this one!! Sorry about that.
…able this to be disabled or overridden on an endpoint by endpoint basis
Looks good, thank you for the PR! |
issue #27
This PR will add the functionality of validating each request against a globally configured request size.
By default, endpoints do not override and will default to the global.
You can override the global configuration inside each endpoint.
ProxyRouterEndpoints have this validation disabled by default