diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java index 5648b260a9..c3532b6b48 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java @@ -26,7 +26,7 @@ import io.netty.util.internal.AppendableCharSequence; import java.util.List; -import static io.netty.buffer.ByteBufUtil.readBytes; +import static io.netty.buffer.ByteBufUtil.*; /** * Decodes {@link ByteBuf}s into {@link HttpMessage}s and @@ -107,11 +107,9 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder maxChunkSize || HttpHeaders.is100ContinueExpected(message)) { - // Generate FullHttpMessage first. HttpChunks will follow. - checkpoint(State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS); - // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT_AS_CHUNKS - // state reads data chunk by chunk. - chunkSize = HttpHeaders.getContentLength(message, -1); - out.add(message); - return; - } - break; - case READ_VARIABLE_LENGTH_CONTENT: - if (buffer.readableBytes() > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) { - // Generate FullHttpMessage first. HttpChunks will follow. - checkpoint(State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS); - out.add(message); - return; - } - break; - default: - throw new IllegalStateException("Unexpected state: " + nextState); + assert nextState == State.READ_FIXED_LENGTH_CONTENT || nextState == State.READ_VARIABLE_LENGTH_CONTENT; + + out.add(message); + + if (nextState == State.READ_FIXED_LENGTH_CONTENT) { + // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk. + chunkSize = HttpHeaders.getContentLength(message, -1); } + // We return here, this forces decode to be called again where we will decode the content return; } catch (Exception e) { @@ -261,35 +245,25 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder maxChunkSize) { - toRead = maxChunkSize; - } - out.add(message); - out.add(new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead))); - return; - } - case READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS: { // Keep reading data as a chunk until the end of connection is reached. - int toRead = actualReadableBytes(); - if (toRead > maxChunkSize) { - toRead = maxChunkSize; - } - ByteBuf content = readBytes(ctx.alloc(), buffer, toRead); - if (!buffer.isReadable()) { + int toRead = Math.min(actualReadableBytes(), maxChunkSize); + if (toRead > 0) { + ByteBuf content = readBytes(ctx.alloc(), buffer, toRead); + if (buffer.isReadable()) { + out.add(new DefaultHttpContent(content)); + } else { + // End of connection. + out.add(new DefaultLastHttpContent(content, validateHeaders)); + reset(); + } + } else if (!buffer.isReadable()) { + // End of connection. + out.add(LastHttpContent.EMPTY_LAST_CONTENT); reset(); - out.add(new DefaultLastHttpContent(content, validateHeaders)); - return; } - out.add(new DefaultHttpContent(content)); return; } case READ_FIXED_LENGTH_CONTENT: { - readFixedLengthContent(ctx, buffer, out); - return; - } - case READ_FIXED_LENGTH_CONTENT_AS_CHUNKS: { - long chunkSize = this.chunkSize; int readLimit = actualReadableBytes(); // Check if the buffer is readable first as we use the readable byte count @@ -302,28 +276,20 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder maxChunkSize) { - toRead = maxChunkSize; - } + int toRead = Math.min(readLimit, maxChunkSize); if (toRead > chunkSize) { toRead = (int) chunkSize; } ByteBuf content = readBytes(ctx.alloc(), buffer, toRead); - if (chunkSize > toRead) { - chunkSize -= toRead; - } else { - chunkSize = 0; - } - this.chunkSize = chunkSize; + chunkSize -= toRead; if (chunkSize == 0) { // Read all content. - reset(); out.add(new DefaultLastHttpContent(content, validateHeaders)); - return; + reset(); + } else { + out.add(new DefaultHttpContent(content)); } - out.add(new DefaultHttpContent(content)); return; } /** @@ -337,9 +303,6 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder maxChunkSize) { - // A chunk is too large. Split them into multiple chunks again. - checkpoint(State.READ_CHUNKED_CONTENT_AS_CHUNKS); } else { checkpoint(State.READ_CHUNKED_CONTENT); } @@ -349,48 +312,19 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder maxChunkSize) { - toRead = maxChunkSize; - } - if (toRead > readLimit) { - toRead = readLimit; - } HttpContent chunk = new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead)); - if (chunkSize > toRead) { - chunkSize -= toRead; - } else { - chunkSize = 0; - } - this.chunkSize = chunkSize; + chunkSize -= toRead; + + out.add(chunk); if (chunkSize == 0) { // Read all content. checkpoint(State.READ_CHUNK_DELIMITER); + } else { + return; } - - out.add(chunk); - return; } case READ_CHUNK_DELIMITER: { for (;;) { @@ -410,16 +344,9 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder= 0 && actualContentLength != expectedContentLength; + prematureClosure = expectedContentLength > 0; } if (!prematureClosure) { - if (actualContentLength == 0) { - out.add(LastHttpContent.EMPTY_LAST_CONTENT); - } else { - out.add(new DefaultLastHttpContent(content, validateHeaders)); - } + out.add(LastHttpContent.EMPTY_LAST_CONTENT); } } } @@ -484,11 +397,8 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder= 100 && code < 200) { - if (code == 101 && !res.headers().contains(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT)) { - // It's Hixie 76 websocket handshake response - return false; - } - return true; + // One exception: Hixie 76 websocket handshake response + return !(code == 101 && !res.headers().contains(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT)); } switch (code) { @@ -500,28 +410,7 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder out) { - if (out != null) { - HttpMessage message = this.message; - ByteBuf content = this.content; - LastHttpContent httpContent; - - if (content == null || !content.isReadable()) { - httpContent = LastHttpContent.EMPTY_LAST_CONTENT; - } else { - httpContent = new DefaultLastHttpContent(content, validateHeaders); - } - - out.add(message); - out.add(httpContent); - } - - content = null; message = null; - checkpoint(State.SKIP_CONTROL_CHARS); } @@ -554,28 +443,6 @@ public abstract class HttpObjectDecoder extends ReplayingDecoder out) { - //we have a content-length so we just read the correct number of bytes - long length = HttpHeaders.getContentLength(message, -1); - assert length <= Integer.MAX_VALUE; - int toRead = (int) length - contentRead; - if (toRead > actualReadableBytes()) { - toRead = actualReadableBytes(); - } - contentRead += toRead; - if (length < contentRead) { - out.add(message); - out.add(new DefaultHttpContent(readBytes(ctx.alloc(), buffer, toRead))); - return; - } - if (content == null) { - content = readBytes(ctx.alloc(), buffer, (int) length); - } else { - content.writeBytes(buffer, (int) length); - } - reset(out); - } - private State readHeaders(ByteBuf buffer) { headerSize = 0; final HttpMessage message = this.message; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpInvalidMessageTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpInvalidMessageTest.java index 430cc6c93a..48ae97927a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpInvalidMessageTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpInvalidMessageTest.java @@ -20,7 +20,6 @@ import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.DecoderResult; import io.netty.util.CharsetUtil; -import org.junit.Ignore; import org.junit.Test; import java.util.Random; @@ -42,7 +41,6 @@ public class HttpInvalidMessageTest { ensureInboundTrafficDiscarded(ch); } - @Ignore("expected ATM") @Test public void testRequestWithBadHeader() throws Exception { EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder()); @@ -70,7 +68,6 @@ public class HttpInvalidMessageTest { ensureInboundTrafficDiscarded(ch); } - @Ignore("expected ATM") @Test public void testResponseWithBadHeader() throws Exception { EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java index cd379ee0ce..f3985be602 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java @@ -19,15 +19,17 @@ package io.netty.handler.codec.http; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.CharsetUtil; -import org.junit.Assert; import org.junit.Test; import java.util.List; +import static org.junit.Assert.*; + public class HttpRequestDecoderTest { private static final byte[] CONTENT_CRLF_DELIMITERS = createContent("\r\n"); private static final byte[] CONTENT_LF_DELIMITERS = createContent("\n"); private static final byte[] CONTENT_MIXED_DELIMITERS = createContent("\r\n", "\n"); + private static final int CONTENT_LENGTH = 8; private static byte[] createContent(String... lineDelimiters) { String lineDelimiter; @@ -46,7 +48,7 @@ public class HttpRequestDecoderTest { "Origin: http://localhost:8080" + lineDelimiter + "Sec-WebSocket-Key1: 10 28 8V7 8 48 0" + lineDelimiter2 + "Sec-WebSocket-Key2: 8 Xt754O3Q3QW 0 _60" + lineDelimiter + - "Content-Length: 8" + lineDelimiter2 + + "Content-Length: " + CONTENT_LENGTH + lineDelimiter2 + "\r\n" + "12345678").getBytes(CharsetUtil.US_ASCII); } @@ -68,34 +70,36 @@ public class HttpRequestDecoderTest { private static void testDecodeWholeRequestAtOnce(byte[] content) { EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); - Assert.assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content))); + assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content))); HttpRequest req = channel.readInbound(); - Assert.assertNotNull(req); + assertNotNull(req); checkHeaders(req.headers()); LastHttpContent c = channel.readInbound(); - Assert.assertEquals(8, c.content().readableBytes()); - Assert.assertEquals(Unpooled.wrappedBuffer(content, content.length - 8, 8), c.content().readBytes(8)); + assertEquals(CONTENT_LENGTH, c.content().readableBytes()); + assertEquals( + Unpooled.wrappedBuffer(content, content.length - CONTENT_LENGTH, CONTENT_LENGTH), + c.content().readBytes(CONTENT_LENGTH)); c.release(); - Assert.assertFalse(channel.finish()); - Assert.assertNull(channel.readInbound()); + assertFalse(channel.finish()); + assertNull(channel.readInbound()); } private static void checkHeaders(HttpHeaders headers) { - Assert.assertEquals(7, headers.names().size()); + assertEquals(7, headers.names().size()); checkHeader(headers, "Upgrade", "WebSocket"); checkHeader(headers, "Connection", "Upgrade"); checkHeader(headers, "Host", "localhost"); checkHeader(headers, "Origin", "http://localhost:8080"); checkHeader(headers, "Sec-WebSocket-Key1", "10 28 8V7 8 48 0"); checkHeader(headers, "Sec-WebSocket-Key2", "8 Xt754O3Q3QW 0 _60"); - checkHeader(headers, "Content-Length", "8"); + checkHeader(headers, "Content-Length", String.valueOf(CONTENT_LENGTH)); } private static void checkHeader(HttpHeaders headers, String name, String value) { List header1 = headers.getAll(name); - Assert.assertEquals(1, header1.size()); - Assert.assertEquals(value, header1.get(0)); + assertEquals(1, header1.size()); + assertEquals(value, header1.get(0)); } @Test @@ -121,7 +125,7 @@ public class HttpRequestDecoderTest { private static void testDecodeWholeRequestInMultipleSteps(byte[] content, int fragmentSize) { EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); - int headerLength = content.length - 8; + int headerLength = content.length - CONTENT_LENGTH; // split up the header for (int a = 0; a < headerLength;) { @@ -136,24 +140,29 @@ public class HttpRequestDecoderTest { a += amount; } - for (int i = 8; i > 0; i--) { + for (int i = CONTENT_LENGTH; i > 0; i --) { // Should produce HttpContent channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1)); } HttpRequest req = channel.readInbound(); - Assert.assertNotNull(req); + assertNotNull(req); checkHeaders(req.headers()); - LastHttpContent c = channel.readInbound(); - Assert.assertEquals(8, c.content().readableBytes()); - for (int i = 8; i > 1; i--) { - Assert.assertEquals(content[content.length - i], c.content().readByte()); + for (int i = CONTENT_LENGTH; i > 1; i --) { + HttpContent c = channel.readInbound(); + assertEquals(1, c.content().readableBytes()); + assertEquals(content[content.length - i], c.content().readByte()); + c.release(); } + + LastHttpContent c = channel.readInbound(); + assertEquals(1, c.content().readableBytes()); + assertEquals(content[content.length - 1], c.content().readByte()); c.release(); - Assert.assertFalse(channel.finish()); - Assert.assertNull(channel.readInbound()); + assertFalse(channel.finish()); + assertNull(channel.readInbound()); } @Test @@ -165,6 +174,6 @@ public class HttpRequestDecoderTest { "EmptyHeader:" + crlf + crlf; channel.writeInbound(Unpooled.wrappedBuffer(request.getBytes(CharsetUtil.US_ASCII))); HttpRequest req = channel.readInbound(); - Assert.assertEquals("", req.headers().get("EmptyHeader")); + assertEquals("", req.headers().get("EmptyHeader")); } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java index 9e4850eba8..3993d12e97 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpHeaders.Names; import io.netty.util.CharsetUtil; import org.junit.Test; @@ -27,14 +28,13 @@ import static org.junit.Assert.*; public class HttpResponseDecoderTest { - /* @Test public void testResponseChunked() { EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII)); - HttpResponse res = (HttpResponse) ch.readInbound(); + HttpResponse res = ch.readInbound(); assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); assertThat(res.getStatus(), is(HttpResponseStatus.OK)); @@ -47,7 +47,7 @@ public class HttpResponseDecoderTest { assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n", CharsetUtil.US_ASCII))); assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); - HttpContent content = (HttpContent) ch.readInbound(); + HttpContent content = ch.readInbound(); assertEquals(data.length, content.content().readableBytes()); byte[] decodedData = new byte[data.length]; @@ -57,22 +57,26 @@ public class HttpResponseDecoderTest { assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII))); } - assertTrue(ch.finish()); - LastHttpContent content = (LastHttpContent) ch.readInbound(); + // Write the last chunk. + ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII)); + + // Ensure the last chunk was decoded. + LastHttpContent content = ch.readInbound(); assertFalse(content.content().isReadable()); content.release(); + ch.finish(); assertNull(ch.readInbound()); } @Test public void testResponseChunkedExceedMaxChunkSize() { EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, 8192, 32)); - ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", - CharsetUtil.US_ASCII)); + ch.writeInbound( + Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII)); - HttpResponse res = (HttpResponse) ch.readInbound(); + HttpResponse res = ch.readInbound(); assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); assertThat(res.getStatus(), is(HttpResponseStatus.OK)); @@ -87,11 +91,11 @@ public class HttpResponseDecoderTest { assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); byte[] decodedData = new byte[data.length]; - HttpContent content = (HttpContent) ch.readInbound(); + HttpContent content = ch.readInbound(); assertEquals(32, content.content().readableBytes()); content.content().readBytes(decodedData, 0, 32); - content = (HttpContent) ch.readInbound(); + content = ch.readInbound(); assertEquals(32, content.content().readableBytes()); content.content().readBytes(decodedData, 32, 32); @@ -101,15 +105,122 @@ public class HttpResponseDecoderTest { assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII))); } - assertTrue(ch.finish()); - LastHttpContent content = (LastHttpContent) ch.readInbound(); + // Write the last chunk. + ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII)); + + // Ensure the last chunk was decoded. + LastHttpContent content = ch.readInbound(); assertFalse(content.content().isReadable()); content.release(); + ch.finish(); assertNull(ch.readInbound()); } - */ + + @Test + public void testClosureWithoutContentLength1() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n\r\n", CharsetUtil.US_ASCII)); + + // Read the response headers. + HttpResponse res = ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + assertThat(ch.readInbound(), is(nullValue())); + + // Close the connection without sending anything. + assertTrue(ch.finish()); + + // The decoder should still produce the last content. + LastHttpContent content = ch.readInbound(); + assertThat(content.content().isReadable(), is(false)); + content.release(); + + // But nothing more. + assertThat(ch.readInbound(), is(nullValue())); + } + + @Test + public void testClosureWithoutContentLength2() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + + // Write the partial response. + ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n\r\n12345678", CharsetUtil.US_ASCII)); + + // Read the response headers. + HttpResponse res = ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + // Read the partial content. + HttpContent content = ch.readInbound(); + assertThat(content.content().toString(CharsetUtil.US_ASCII), is("12345678")); + assertThat(content, is(not(instanceOf(LastHttpContent.class)))); + content.release(); + + assertThat(ch.readInbound(), is(nullValue())); + + // Close the connection. + assertTrue(ch.finish()); + + // The decoder should still produce the last content. + LastHttpContent lastContent = ch.readInbound(); + assertThat(lastContent.content().isReadable(), is(false)); + lastContent.release(); + + // But nothing more. + assertThat(ch.readInbound(), is(nullValue())); + } + + @Test + public void testPrematureClosureWithChunkedEncoding1() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + ch.writeInbound( + Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII)); + + // Read the response headers. + HttpResponse res = ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked")); + assertThat(ch.readInbound(), is(nullValue())); + + // Close the connection without sending anything. + ch.finish(); + + // The decoder should not generate the last chunk because it's closed prematurely. + assertThat(ch.readInbound(), is(nullValue())); + } + + @Test + public void testPrematureClosureWithChunkedEncoding2() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + + // Write the partial response. + ch.writeInbound(Unpooled.copiedBuffer( + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n8\r\n12345678", CharsetUtil.US_ASCII)); + + // Read the response headers. + HttpResponse res = ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked")); + + // Read the partial content. + HttpContent content = ch.readInbound(); + assertThat(content.content().toString(CharsetUtil.US_ASCII), is("12345678")); + assertThat(content, is(not(instanceOf(LastHttpContent.class)))); + content.release(); + + assertThat(ch.readInbound(), is(nullValue())); + + // Close the connection. + ch.finish(); + + // The decoder should not generate the last chunk because it's closed prematurely. + assertThat(ch.readInbound(), is(nullValue())); + } @Test public void testLastResponseWithEmptyHeaderAndEmptyContent() { @@ -281,10 +392,15 @@ public class HttpResponseDecoderTest { assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); assertThat(res.getStatus(), is(HttpResponseStatus.OK)); - LastHttpContent content = ch.readInbound(); - assertEquals(10, content.content().readableBytes()); - assertEquals(Unpooled.wrappedBuffer(data), content.content()); - content.release(); + HttpContent firstContent = ch.readInbound(); + assertThat(firstContent.content().readableBytes(), is(5)); + assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content()); + firstContent.release(); + + LastHttpContent lastContent = ch.readInbound(); + assertEquals(5, lastContent.content().readableBytes()); + assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content()); + lastContent.release(); assertThat(ch.finish(), is(false)); assertThat(ch.readInbound(), is(nullValue())); @@ -324,9 +440,14 @@ public class HttpResponseDecoderTest { assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + HttpContent firstContent = ch.readInbound(); + assertThat(firstContent.content().readableBytes(), is(5)); + assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content()); + firstContent.release(); + LastHttpContent lastContent = ch.readInbound(); - assertEquals(10, lastContent.content().readableBytes()); - assertEquals(Unpooled.wrappedBuffer(data), lastContent.content()); + assertEquals(5, lastContent.content().readableBytes()); + assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content()); lastContent.release(); assertThat(ch.finish(), is(false));