From 0f76b3c357fbb3500a9f0742e9369139b3283ceb Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Tue, 17 Dec 2013 00:17:35 +0900 Subject: [PATCH] Remove unnecessary code from HttpObjectDecoder and re-enable all HTTP tests - Since Netty 4, HTTP decoder does not generate a full message at all. Therefore, there's no need to keep separate states for the content smaller than maxChunkSize. - maxChunkSize must be greater than 0. Setting it to 0 should not disable chunked encoding. We have a dedicated flag for that. - Uncommented the tests that were commented out for an unknown reason, with some fixes. - Added more tests for HTTP decoder. - Removed the Ignore annotation on some tests. --- .../handler/codec/http/HttpObjectDecoder.java | 229 +++-------- .../codec/http/HttpRequestDecoderTest.java | 179 ++++++++ .../codec/http/HttpResponseDecoderTest.java | 384 ++++++++++++++++++ 3 files changed, 611 insertions(+), 181 deletions(-) create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java 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/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java new file mode 100644 index 0000000000..aed1b84dd9 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2013 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: + * + * http://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.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.CharsetUtil; +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; + String lineDelimiter2; + if (lineDelimiters.length == 2) { + lineDelimiter = lineDelimiters[0]; + lineDelimiter2 = lineDelimiters[1]; + } else { + lineDelimiter = lineDelimiters[0]; + lineDelimiter2 = lineDelimiters[0]; + } + return ("GET /some/path?foo=bar&wibble=eek HTTP/1.1" + "\r\n" + + "Upgrade: WebSocket" + lineDelimiter2 + + "Connection: Upgrade" + lineDelimiter + + "Host: localhost" + lineDelimiter2 + + "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: " + CONTENT_LENGTH + lineDelimiter2 + + "\r\n" + + "12345678").getBytes(CharsetUtil.US_ASCII); + } + + @Test + public void testDecodeWholeRequestAtOnceCRLFDelimiters() { + testDecodeWholeRequestAtOnce(CONTENT_CRLF_DELIMITERS); + } + + @Test + public void testDecodeWholeRequestAtOnceLFDelimiters() { + testDecodeWholeRequestAtOnce(CONTENT_LF_DELIMITERS); + } + + @Test + public void testDecodeWholeRequestAtOnceMixedDelimiters() { + testDecodeWholeRequestAtOnce(CONTENT_MIXED_DELIMITERS); + } + + private static void testDecodeWholeRequestAtOnce(byte[] content) { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content))); + HttpRequest req = (HttpRequest) channel.readInbound(); + assertNotNull(req); + checkHeaders(req.headers()); + LastHttpContent c = (LastHttpContent) channel.readInbound(); + assertEquals(CONTENT_LENGTH, c.content().readableBytes()); + assertEquals( + Unpooled.wrappedBuffer(content, content.length - CONTENT_LENGTH, CONTENT_LENGTH), + c.content().readBytes(CONTENT_LENGTH)); + c.release(); + + assertFalse(channel.finish()); + assertNull(channel.readInbound()); + } + + private static void checkHeaders(HttpHeaders headers) { + 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", String.valueOf(CONTENT_LENGTH)); + } + + private static void checkHeader(HttpHeaders headers, String name, String value) { + List header1 = headers.getAll(name); + assertEquals(1, header1.size()); + assertEquals(value, header1.get(0)); + } + + @Test + public void testDecodeWholeRequestInMultipleStepsCRLFDelimiters() { + testDecodeWholeRequestInMultipleSteps(CONTENT_CRLF_DELIMITERS); + } + + @Test + public void testDecodeWholeRequestInMultipleStepsLFDelimiters() { + testDecodeWholeRequestInMultipleSteps(CONTENT_LF_DELIMITERS); + } + + @Test + public void testDecodeWholeRequestInMultipleStepsMixedDelimiters() { + testDecodeWholeRequestInMultipleSteps(CONTENT_MIXED_DELIMITERS); + } + + private static void testDecodeWholeRequestInMultipleSteps(byte[] content) { + for (int i = 1; i < content.length; i++) { + testDecodeWholeRequestInMultipleSteps(content, i); + } + } + + private static void testDecodeWholeRequestInMultipleSteps(byte[] content, int fragmentSize) { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + int headerLength = content.length - CONTENT_LENGTH; + + // split up the header + for (int a = 0; a < headerLength;) { + int amount = fragmentSize; + if (a + amount > headerLength) { + amount = headerLength - a; + } + + // if header is done it should produce a HttpRequest + boolean headerDone = a + amount == headerLength; + channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount)); + a += amount; + } + + for (int i = CONTENT_LENGTH; i > 0; i --) { + // Should produce HttpContent + channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1)); + } + + HttpRequest req = (HttpRequest) channel.readInbound(); + assertNotNull(req); + checkHeaders(req.headers()); + + for (int i = CONTENT_LENGTH; i > 1; i --) { + HttpContent c = (HttpContent) channel.readInbound(); + assertEquals(1, c.content().readableBytes()); + assertEquals(content[content.length - i], c.content().readByte()); + c.release(); + } + + LastHttpContent c = (LastHttpContent) channel.readInbound(); + assertEquals(1, c.content().readableBytes()); + assertEquals(content[content.length - 1], c.content().readByte()); + c.release(); + + assertFalse(channel.finish()); + assertNull(channel.readInbound()); + } + + @Test + public void testEmptyHeaderValue() { + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + String crlf = "\r\n"; + String request = "GET /some/path HTTP/1.1" + crlf + + "Host: localhost" + crlf + + "EmptyHeader:" + crlf + crlf; + channel.writeInbound(Unpooled.wrappedBuffer(request.getBytes(CharsetUtil.US_ASCII))); + HttpRequest req = (HttpRequest) channel.readInbound(); + 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 e83c4e8423..4d5f8d95f3 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,13 +17,211 @@ 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; +import java.util.List; + import static org.hamcrest.CoreMatchers.*; 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(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + byte[] data = new byte[64]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + + for (int i = 0; i < 10; i++) { + 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(); + assertEquals(data.length, content.content().readableBytes()); + + byte[] decodedData = new byte[data.length]; + content.content().readBytes(decodedData); + assertArrayEquals(data, decodedData); + content.release(); + + assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII))); + } + + // Write the last chunk. + ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII)); + + // Ensure the last chunk was decoded. + LastHttpContent content = (LastHttpContent) 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)); + + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + byte[] data = new byte[64]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + + for (int i = 0; i < 10; i++) { + assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n", + CharsetUtil.US_ASCII))); + assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data))); + + byte[] decodedData = new byte[data.length]; + HttpContent content = (HttpContent) ch.readInbound(); + assertEquals(32, content.content().readableBytes()); + content.content().readBytes(decodedData, 0, 32); + + content = (HttpContent) ch.readInbound(); + assertEquals(32, content.content().readableBytes()); + + content.content().readBytes(decodedData, 32, 32); + + assertArrayEquals(data, decodedData); + content.release(); + + assertFalse(ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII))); + } + + // Write the last chunk. + ch.writeInbound(Unpooled.copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII)); + + // Ensure the last chunk was decoded. + LastHttpContent content = (LastHttpContent) 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 = (HttpResponse) 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 = (LastHttpContent) 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 = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + // Read the partial content. + HttpContent content = (HttpContent) 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 = (LastHttpContent) 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 = (HttpResponse) 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 = (HttpResponse) 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 = (HttpContent) 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() { EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); @@ -66,4 +264,190 @@ public class HttpResponseDecoderTest { assertThat(ch.readInbound(), is(nullValue())); } + + @Test + public void testLastResponseWithTrailingHeader() { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + ch.writeInbound(Unpooled.copiedBuffer( + "HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "0\r\n" + + "Set-Cookie: t1=t1v1\r\n" + + "Set-Cookie: t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT\r\n" + + "\r\n", + CharsetUtil.US_ASCII)); + + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + LastHttpContent lastContent = (LastHttpContent) ch.readInbound(); + assertThat(lastContent.content().isReadable(), is(false)); + HttpHeaders headers = lastContent.trailingHeaders(); + assertEquals(1, headers.names().size()); + List values = headers.getAll("Set-Cookie"); + assertEquals(2, values.size()); + assertTrue(values.contains("t1=t1v1")); + assertTrue(values.contains("t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT")); + lastContent.release(); + + assertThat(ch.finish(), is(false)); + assertThat(ch.readInbound(), is(nullValue())); + } + + @Test + public void testLastResponseWithTrailingHeaderFragmented() { + byte[] data = ("HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "0\r\n" + + "Set-Cookie: t1=t1v1\r\n" + + "Set-Cookie: t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT\r\n" + + "\r\n").getBytes(CharsetUtil.US_ASCII); + + for (int i = 1; i < data.length; i++) { + testLastResponseWithTrailingHeaderFragmented(data, i); + } + } + + private static void testLastResponseWithTrailingHeaderFragmented(byte[] content, int fragmentSize) { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + int headerLength = 47; + // split up the header + for (int a = 0; a < headerLength;) { + int amount = fragmentSize; + if (a + amount > headerLength) { + amount = headerLength - a; + } + + // if header is done it should produce a HttpRequest + boolean headerDone = a + amount == headerLength; + assertEquals(headerDone, ch.writeInbound(Unpooled.wrappedBuffer(content, a, amount))); + a += amount; + } + + ch.writeInbound(Unpooled.wrappedBuffer(content, headerLength, content.length - headerLength)); + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + LastHttpContent lastContent = (LastHttpContent) ch.readInbound(); + assertThat(lastContent.content().isReadable(), is(false)); + HttpHeaders headers = lastContent.trailingHeaders(); + assertEquals(1, headers.names().size()); + List values = headers.getAll("Set-Cookie"); + assertEquals(2, values.size()); + assertTrue(values.contains("t1=t1v1")); + assertTrue(values.contains("t2=t2v2; Expires=Wed, 09-Jun-2021 10:18:14 GMT")); + lastContent.release(); + + assertThat(ch.finish(), is(false)); + assertThat(ch.readInbound(), is(nullValue())); + } + + @Test + public void testResponseWithContentLength() { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + ch.writeInbound(Unpooled.copiedBuffer( + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 10\r\n" + + "\r\n", CharsetUtil.US_ASCII)); + + byte[] data = new byte[10]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2)); + ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2)); + + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + HttpContent firstContent = (HttpContent) ch.readInbound(); + assertThat(firstContent.content().readableBytes(), is(5)); + assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content()); + firstContent.release(); + + LastHttpContent lastContent = (LastHttpContent) 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())); + } + + @Test + public void testResponseWithContentLengthFragmented() { + byte[] data = ("HTTP/1.1 200 OK\r\n" + + "Content-Length: 10\r\n" + + "\r\n").getBytes(CharsetUtil.US_ASCII); + + for (int i = 1; i < data.length; i++) { + testResponseWithContentLengthFragmented(data, i); + } + } + + private static void testResponseWithContentLengthFragmented(byte[] header, int fragmentSize) { + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + // split up the header + for (int a = 0; a < header.length;) { + int amount = fragmentSize; + if (a + amount > header.length) { + amount = header.length - a; + } + + ch.writeInbound(Unpooled.wrappedBuffer(header, a, amount)); + a += amount; + } + byte[] data = new byte[10]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) i; + } + ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2)); + ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2)); + + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.OK)); + + HttpContent firstContent = (HttpContent) ch.readInbound(); + assertThat(firstContent.content().readableBytes(), is(5)); + assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content()); + firstContent.release(); + + LastHttpContent lastContent = (LastHttpContent) 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())); + } + + @Test + public void testWebSocketResponse() { + byte[] data = ("HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Origin: http://localhost:8080\r\n" + + "Sec-WebSocket-Location: ws://localhost/some/path\r\n" + + "\r\n" + + "1234567812345678").getBytes(); + EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder()); + ch.writeInbound(Unpooled.wrappedBuffer(data)); + + HttpResponse res = (HttpResponse) ch.readInbound(); + assertThat(res.getProtocolVersion(), sameInstance(HttpVersion.HTTP_1_1)); + assertThat(res.getStatus(), is(HttpResponseStatus.SWITCHING_PROTOCOLS)); + HttpContent content = (HttpContent) ch.readInbound(); + assertThat(content.content().readableBytes(), is(16)); + content.release(); + + assertThat(ch.finish(), is(false)); + + assertThat(ch.readInbound(), is(nullValue())); + } }