Skip to content

Commit e4ba847

Browse files
committed
Allow for multiple Content-Length headers behavior to be configurable...
...and introduce HttpDecoderOption & HttpDecoderBuilder in doing so. Motivation Since netty#9865 (Netty 4.1.44) the default behavior of the HttpObjectDecoder has been to reject any HTTP message that is found to have multiple Content-Length headers when decoding. This behavior is well-justified as per the risks outlined in netty#9861, however, we can see from the cited RFC section that there are multiple possible options offered for responding to this scenario: > If a message is received that has multiple Content-Length header > fields with field-values consisting of the same decimal value, or a > single Content-Length header field with a field value containing a > list of identical decimal values (e.g., "Content-Length: 42, 42"), > indicating that duplicate Content-Length header fields have been > generated or combined by an upstream message processor, then the > recipient MUST either reject the message as invalid or replace the > duplicated field-values with a single valid Content-Length field > containing that decimal value prior to determining the message body > length or forwarding the message. https://tools.ietf.org/html/rfc7230#section-3.3.2 Netty opted for the first option (rejecting as invalid), which seems like the safest, but the second option (replacing duplicate values with a single value) is also valid behavior. While it makes sense for Netty to bias towards the safest option, Netty is used as a library for a variety of different use cases, and there is usually no one-size-fits-all for parsing behavior like this... It seems like the ideal solution would be to default to the safest behavior but to also make these types of decisions configurable. This is further motivated by the fact that the HTTP RFCs are often vague or ambiguous in many areas and we have seen other back-and-forth conversations (e.g., netty#10003) based on differing interpretations that would probably be most amicably resolved by simply exposing better configuration. Modifications * Deprecate HttpRequestDecoder's telescoping constructors and introduce AbstractHttpObjectDecoderBuilder and HttpRequestDecoderBuilder. (The current HttpObjectDecoder implementations are only minimally configurable, and any further configuration will not scale well with the existing telescoping constructors.) * Move default decoder values to the HttpObjectDecoder class and reference them accordingly. * Introduce HttpDecoderOption class. This option is similar in vein to the ChannelOption class, where it is capable of exposing a wide possibility of different configurations. * Introduce the first decoder option: MultipleContentLengthHeadersBehavior. This option sets the precedent of using an interface for its implementation (as opposed to boolean/enum) so that users can easily define their own implementations or even "listen in" on implementations for logging purposes (without having to override). * Current available options are: ALWAYS_REJECT, IF_DIFFERENT_REJECT_ELSE_DEDUPE, and IF_DIFFERENT_REJECT_ELSE_ALLOW (see Javadoc for more). * Note that the existing logic would result in NumberFormatExceptions for header values like "Content-Length: 42, 42". The new logic correctly reports these as IllegalArgumentException. * Extend HttpObjectDecoder to accept a map of options and use these options to resolve multiple Content-Length scenarios. * In doing so, also apply this logic to HTTP/1.0 (in addition to 1.1). Even though the cited RFCs were for 1.1, it seems that the risks they are trying to mitigate could still apply to messages in 1.0 that specify a content length. * Also include HTTP/1.0 in the existing "handleTransferEncodingChunkedWithContentLength" logic for the same reason (this logic could also probably be better served by an HttpDecoderOption in the future). * Include new unit tests to test all of the possible combinations of MultipleContentLengthHeadersBehavior and multiple content lengths. Result This is a backwards-compatible change with no functional change to the existing behavior. Users will now be able to customize the behavior for handling multiple content lengths, and we will be able to more easily add additional options in the future if needed. To limit the scope of this commit, it does not attempt to introduce the builder or decoder options to the other relevant classes (HttpResponseDecoder, HttpClientCodec, HttpServerCodec), but we may wish to follow-up with those changes.
1 parent bafd089 commit e4ba847

File tree

2 files changed

+10
-11
lines changed

2 files changed

+10
-11
lines changed

codec-http/src/main/java/io/netty/handler/codec/http/HttpDecoderOption.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public interface MultipleContentLengthHeadersBehavior {
111111
* that the {@link List} of values represents different header lines and has not been split on commas. A single
112112
* line may or may not have multiple, comma-separated values.
113113
*/
114-
long resolveContentLength(HttpHeaders headers, List<String> contentLengthFields);
114+
long resolveContentLength(HttpHeaders headers, List<String> contentLengthFieldValues);
115115

116116
/**
117117
* {@link #ALWAYS_REJECT} will always throw an {@link IllegalArgumentException} (and thus trigger a failed
@@ -124,7 +124,7 @@ public interface MultipleContentLengthHeadersBehavior {
124124
MultipleContentLengthHeadersBehavior ALWAYS_REJECT =
125125
new MultipleContentLengthHeadersBehavior() {
126126
@Override
127-
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFields) {
127+
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFieldValues) {
128128
throw new IllegalArgumentException("Multiple Content-Length headers found");
129129
}
130130
};
@@ -139,8 +139,8 @@ public long resolveContentLength(HttpHeaders headers, List<String> contentLength
139139
MultipleContentLengthHeadersBehavior IF_DIFFERENT_REJECT_ELSE_DEDUPE =
140140
new MultipleContentLengthHeadersBehavior() {
141141
@Override
142-
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFields) {
143-
String contentLength = findAndEnforceUniqueContentLength(contentLengthFields);
142+
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFieldValues) {
143+
String contentLength = findAndEnforceUniqueContentLength(contentLengthFieldValues);
144144
headers.set(HttpHeaderNames.CONTENT_LENGTH, contentLength);
145145
return Long.parseLong(contentLength);
146146
}
@@ -151,13 +151,13 @@ public long resolveContentLength(HttpHeaders headers, List<String> contentLength
151151
* all the same. When this occurs, the original {@link HttpHeaders} (and its multiple values) are left
152152
* untouched.
153153
* <p>
154-
* This behavior is not offered as an explicit option in the RFC but may desired for pass-through behavior.
154+
* This behavior is not offered as an explicit option in the RFC but may be desired for pass-through behavior.
155155
*/
156156
MultipleContentLengthHeadersBehavior IF_DIFFERENT_REJECT_ELSE_ALLOW =
157157
new MultipleContentLengthHeadersBehavior() {
158158
@Override
159-
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFields) {
160-
String contentLength = findAndEnforceUniqueContentLength(contentLengthFields);
159+
public long resolveContentLength(HttpHeaders headers, List<String> contentLengthFieldValues) {
160+
String contentLength = findAndEnforceUniqueContentLength(contentLengthFieldValues);
161161
return Long.parseLong(contentLength);
162162
}
163163
};

codec-http/src/test/java/io/netty/handler/codec/http/MultipleContentLengthHeadersBehaviorTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public class MultipleContentLengthHeadersBehaviorTest {
4444
private final boolean singleField;
4545

4646
private EmbeddedChannel channel;
47-
private HttpRequest request;
4847

4948
@Parameters
5049
public static Collection<Object[]> parameters() {
@@ -83,7 +82,7 @@ public void setUp() {
8382
public void testMultipleContentLengthHeadersBehavior() {
8483
String requestStr = setupRequestString();
8584
assertThat(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)), is(true));
86-
request = channel.readInbound();
85+
HttpRequest request = channel.readInbound();
8786

8887
if (behavior == MultipleContentLengthHeadersBehavior.ALWAYS_REJECT) {
8988
assertInvalid(request);
@@ -113,7 +112,7 @@ public void testMultipleContentLengthHeadersBehavior() {
113112

114113
private String setupRequestString() {
115114
String firstValue = "1";
116-
String secondValue = sameValue? firstValue : "2";
115+
String secondValue = sameValue ? firstValue : "2";
117116
String contentLength;
118117
if (singleField) {
119118
contentLength = "Content-Length: " + firstValue + ", " + secondValue + "\r\n\r\n";
@@ -133,7 +132,7 @@ public void testDanglingComma() {
133132
"Connection: close\n\n" +
134133
"b";
135134
assertThat(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)), is(true));
136-
request = channel.readInbound();
135+
HttpRequest request = channel.readInbound();
137136
assertInvalid(request);
138137
assertThat(channel.finish(), is(false));
139138
}

0 commit comments

Comments
 (0)