Introduce HttpMessageDecoderResult to expose decoded header size (#11068)

Motivation

The HttpObjectDecoder accepts input parameters for maxInitialLineLength
and maxHeaderSize. These are important variables since both message
components must be buffered in memory. As such, many decoders (like
Netty and others) introduce constraints. Due to their importance, many
users may wish to add instrumentation on the values of successful
decoder results, or otherwise be able to access these values to enforce
their own supplemental constraints.

While users can perhaps estimate the sizes today, they will not be
exact, due to the decoder being responsible for consuming optional
whitespace and the like.

Modifications

* Add HttpMessageDecoderResult class. This class extends DecoderResult
and is intended for HttpMessage objects successfully decoded by the
HttpObjectDecoder. It exposes attributes for the decoded
initialLineLength and headerSize.
* Modify HttpObjectDecoder to produce HttpMessageDecoderResults upon
successfully decoding the last HTTP header.
* Add corresponding tests to HttpRequestDecoderTest &
HttpResponseDecoderTest.

Co-authored-by: Bennett Lynch <Bennett-Lynch@users.noreply.github.com>
This commit is contained in:
Bennett Lynch 2021-03-12 04:49:51 -08:00 committed by GitHub
parent 24b5a21e46
commit 3f23f59b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 1 deletions

View File

@ -0,0 +1,58 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http;
import io.netty.handler.codec.DecoderResult;
/**
* A {@link DecoderResult} for {@link HttpMessage}s as produced by an {@link HttpObjectDecoder}.
* <p>
* Please note that there is no guarantee that a {@link HttpObjectDecoder} will produce a {@link
* HttpMessageDecoderResult}. It may simply produce a regular {@link DecoderResult}. This result is intended for
* successful {@link HttpMessage} decoder results.
*/
public final class HttpMessageDecoderResult extends DecoderResult {
private final int initialLineLength;
private final int headerSize;
HttpMessageDecoderResult(int initialLineLength, int headerSize) {
super(SIGNAL_SUCCESS);
this.initialLineLength = initialLineLength;
this.headerSize = headerSize;
}
/**
* The decoded initial line length (in bytes), as controlled by {@code maxInitialLineLength}.
*/
public int initialLineLength() {
return initialLineLength;
}
/**
* The decoded header size (in bytes), as controlled by {@code maxHeaderSize}.
*/
public int headerSize() {
return headerSize;
}
/**
* The decoded initial line length plus the decoded header size (in bytes).
*/
public int totalSize() {
return initialLineLength + headerSize;
}
}

View File

@ -628,6 +628,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
name = null; name = null;
value = null; value = null;
// Done parsing initial line and headers. Set decoder result.
HttpMessageDecoderResult decoderResult = new HttpMessageDecoderResult(lineParser.size, headerParser.size);
message.setDecoderResult(decoderResult);
List<String> contentLengthFields = headers.getAll(HttpHeaderNames.CONTENT_LENGTH); List<String> contentLengthFields = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
if (!contentLengthFields.isEmpty()) { if (!contentLengthFields.isEmpty()) {
HttpVersion version = message.protocolVersion(); HttpVersion version = message.protocolVersion();
@ -893,7 +897,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
private static class HeaderParser implements ByteProcessor { private static class HeaderParser implements ByteProcessor {
private final AppendableCharSequence seq; private final AppendableCharSequence seq;
private final int maxLength; private final int maxLength;
private int size; int size;
HeaderParser(AppendableCharSequence seq, int maxLength) { HeaderParser(AppendableCharSequence seq, int maxLength) {
this.seq = seq; this.seq = seq;

View File

@ -20,6 +20,7 @@ import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.TooLongFrameException;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
@ -478,6 +479,26 @@ public class HttpRequestDecoderTest {
assertFalse(channel.finish()); assertFalse(channel.finish());
} }
@Test
public void testHttpMessageDecoderResult() {
String requestStr = "PUT /some/path HTTP/1.1\r\n" +
"Content-Length: 11\r\n" +
"Connection: close\r\n\r\n" +
"Lorem ipsum";
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
HttpRequest request = channel.readInbound();
assertTrue(request.decoderResult().isSuccess());
assertThat(request.decoderResult(), instanceOf(HttpMessageDecoderResult.class));
HttpMessageDecoderResult decoderResult = (HttpMessageDecoderResult) request.decoderResult();
assertThat(decoderResult.initialLineLength(), is(23));
assertThat(decoderResult.headerSize(), is(35));
assertThat(decoderResult.totalSize(), is(58));
HttpContent c = channel.readInbound();
c.release();
assertFalse(channel.finish());
}
private static void testInvalidHeaders0(String requestStr) { private static void testInvalidHeaders0(String requestStr) {
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));

View File

@ -727,4 +727,24 @@ public class HttpResponseDecoderTest {
assertEquals("netty.io", response.headers().get(HttpHeaderNames.HOST)); assertEquals("netty.io", response.headers().get(HttpHeaderNames.HOST));
assertFalse(channel.finish()); assertFalse(channel.finish());
} }
@Test
public void testHttpMessageDecoderResult() {
String responseStr = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n" +
"Connection: close\r\n\r\n" +
"Lorem ipsum";
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseDecoder());
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(responseStr, CharsetUtil.US_ASCII)));
HttpResponse response = channel.readInbound();
assertTrue(response.decoderResult().isSuccess());
assertThat(response.decoderResult(), instanceOf(HttpMessageDecoderResult.class));
HttpMessageDecoderResult decoderResult = (HttpMessageDecoderResult) response.decoderResult();
assertThat(decoderResult.initialLineLength(), is(15));
assertThat(decoderResult.headerSize(), is(35));
assertThat(decoderResult.totalSize(), is(50));
HttpContent c = channel.readInbound();
c.release();
assertFalse(channel.finish());
}
} }