From 386a06dbfa13f981aff59a9abb7cbecee2701e65 Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Tue, 5 Aug 2014 15:22:24 -0700 Subject: [PATCH] HttpContentCompressor doesn't need to generate anything if Content-Length is 0 Motivation: When Content-Encoding is deflate or gzip and Content-Length is 0, there's no need to generate an empty stream (e.g. 20-byte gzip stream). We can just produce nothing. At least, it works fine with most modern browsers. Also, we can skip the instantiation of an encoder entirely by instantiating it lazily. Modifications: Backport HttpContentEncoderTest from 4.0 Add similar tests for HttpContentCompressor to ensure the same tests work with compression. Result: Fixes issue #2321 --- .../codec/http/HttpContentEncoder.java | 15 +- .../codec/http/HttpContentCompressorTest.java | 259 +++++++++++++++- .../codec/http/HttpContentEncoderTest.java | 285 ++++++++++++++++++ 3 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/jboss/netty/handler/codec/http/HttpContentEncoderTest.java diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java index 77c1a87934..597328c357 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java @@ -15,9 +15,6 @@ */ package org.jboss.netty.handler.codec.http; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelHandlerContext; @@ -28,6 +25,9 @@ import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + /** * Encodes the content of the outbound {@link HttpResponse} and {@link HttpChunk}. * The original content is replaced with the new content encoded by the @@ -54,6 +54,7 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler private final Queue acceptEncodingQueue = new ConcurrentLinkedQueue(); private volatile EncoderEmbedder encoder; + private volatile boolean offerred; /** * Creates a new instance. @@ -198,16 +199,24 @@ public abstract class HttpContentEncoder extends SimpleChannelHandler protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception; private ChannelBuffer encode(ChannelBuffer buf) { + offerred = true; encoder.offer(buf); return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()])); } private ChannelBuffer finishEncode() { if (encoder == null) { + offerred = false; return ChannelBuffers.EMPTY_BUFFER; } ChannelBuffer result; + if (!offerred) { + // No data was offerred to the encoder since the encoder was created. + // We should offer at least an empty buffer so that the encoder knows its is encoding empty content. + offerred = false; + encoder.offer(ChannelBuffers.EMPTY_BUFFER); + } if (encoder.finish()) { result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()])); } else { diff --git a/src/test/java/org/jboss/netty/handler/codec/http/HttpContentCompressorTest.java b/src/test/java/org/jboss/netty/handler/codec/http/HttpContentCompressorTest.java index fbf9b1147d..d10b127798 100644 --- a/src/test/java/org/jboss/netty/handler/codec/http/HttpContentCompressorTest.java +++ b/src/test/java/org/jboss/netty/handler/codec/http/HttpContentCompressorTest.java @@ -15,9 +15,17 @@ */ package org.jboss.netty.handler.codec.http; -import org.junit.Assert; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpHeaders.Values; +import org.jboss.netty.util.CharsetUtil; import org.junit.Test; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + public class HttpContentCompressorTest { @Test @@ -40,7 +48,254 @@ public class HttpContentCompressorTest { String acceptEncoding = tests[i]; String contentEncoding = tests[i + 1]; String targetEncoding = compressor.getTargetContentEncoding(acceptEncoding); - Assert.assertEquals(contentEncoding, targetEncoding); + assertEquals(contentEncoding, targetEncoding); } } + + static final class HttpContentCompressorEmbedder extends EncoderEmbedder { + HttpContentCompressorEmbedder() { + super(new HttpContentCompressor()); + } + + public void fireMessageReceived(Object msg) { + Channels.fireMessageReceived(getChannel(), msg, null); + } + } + + @Test + public void testSplitContent() throws Exception { + HttpContentCompressorEmbedder e = new HttpContentCompressorEmbedder(); + e.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setChunked(true); + e.offer(res); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("Hell", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("o, w", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("orld", CharsetUtil.US_ASCII))); + e.offer(HttpChunk.LAST_CHUNK); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.isChunked(), is(true)); + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("gzip")); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("1f8b0800000000000000f248cdc901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("cad7512807000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("ca2fca4901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("0300c2a99ae70c000000")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testChunkedContent() throws Exception { + HttpContentCompressorEmbedder e = new HttpContentCompressorEmbedder(); + e.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED); + e.offer(res); + + assertEncodedResponse(e); + + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("Hell", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("o, w", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("orld", CharsetUtil.US_ASCII))); + e.offer(HttpChunk.LAST_CHUNK); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("1f8b0800000000000000f248cdc901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("cad7512807000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("ca2fca4901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("0300c2a99ae70c000000")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testChunkedContentWithTrailingHeader() throws Exception { + HttpContentCompressorEmbedder e = new HttpContentCompressorEmbedder(); + e.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED); + e.offer(res); + + assertEncodedResponse(e); + + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("Hell", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("o, w", CharsetUtil.US_ASCII))); + e.offer(new DefaultHttpChunk(ChannelBuffers.copiedBuffer("orld", CharsetUtil.US_ASCII))); + HttpChunkTrailer trailer = new DefaultHttpChunkTrailer(); + trailer.trailingHeaders().set("X-Test", "Netty"); + e.offer(trailer); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("1f8b0800000000000000f248cdc901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("cad7512807000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("ca2fca4901000000ffff")); + + chunk = (HttpChunk) e.poll(); + assertThat(ChannelBuffers.hexDump(chunk.getContent()), is("0300c2a99ae70c000000")); + assertThat(chunk, is(instanceOf(HttpChunk.class))); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + assertEquals("Netty", ((HttpChunkTrailer) chunk).trailingHeaders().get("X-Test")); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testFullContent() throws Exception { + HttpContentCompressorEmbedder e = new HttpContentCompressorEmbedder(); + e.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setContent(ChannelBuffers.copiedBuffer("Hello, World", CharsetUtil.US_ASCII)); + res.headers().set(Names.CONTENT_LENGTH, res.getContent().readableBytes()); + e.offer(res); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.isChunked(), is(false)); + assertThat( + ChannelBuffers.hexDump(res.getContent()), + is("1f8b0800000000000000f248cdc9c9d75108cf2fca4901000000ffff0300c6865b260c000000")); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(String.valueOf(res.getContent().readableBytes()))); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("gzip")); + + assertThat(e.poll(), is(nullValue())); + } + + /** + * If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content + * even if the actual length is turned out to be 0. + */ + @Test + public void testEmptySplitContent() throws Exception { + HttpContentCompressorEmbedder e = new HttpContentCompressorEmbedder(); + e.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setChunked(true); + e.offer(res); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.isChunked(), is(true)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("gzip")); + + e.offer(HttpChunk.LAST_CHUNK); + + HttpChunk chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readableBytes(), is(20)); // an empty gzip stream is 20 bytes long. + assertThat(chunk, is(instanceOf(HttpChunk.class))); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + /** + * If the length of the content is 0 for sure, {@link HttpContentEncoder} should skip encoding. + */ + @Test + public void testEmptyFullContent() throws Exception { + HttpContentCompressorEmbedder ch = new HttpContentCompressorEmbedder(); + ch.fireMessageReceived(newRequest()); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setContent(ChannelBuffers.EMPTY_BUFFER); + ch.offer(res); + + Object o = ch.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = ch.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + + // Content encoding shouldn't be modified. + assertThat(res.headers().get(Names.CONTENT_ENCODING), is(nullValue())); + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.getContent().toString(CharsetUtil.US_ASCII), is("")); + + assertThat(ch.poll(), is(nullValue())); + } + + private static HttpRequest newRequest() { + HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + req.headers().set(Names.ACCEPT_ENCODING, "gzip"); + return req; + } + + private static void assertEncodedResponse(HttpContentCompressorEmbedder e) { + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + HttpResponse res = (HttpResponse) o; + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked")); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("gzip")); + } } diff --git a/src/test/java/org/jboss/netty/handler/codec/http/HttpContentEncoderTest.java b/src/test/java/org/jboss/netty/handler/codec/http/HttpContentEncoderTest.java new file mode 100644 index 0000000000..fcd72305c4 --- /dev/null +++ b/src/test/java/org/jboss/netty/handler/codec/http/HttpContentEncoderTest.java @@ -0,0 +1,285 @@ +/* + * Copyright 2014 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 org.jboss.netty.handler.codec.http; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpHeaders.Values; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; +import org.jboss.netty.util.CharsetUtil; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +public class HttpContentEncoderTest { + + private static final class TestEncoder extends HttpContentEncoder { + @Override + protected EncoderEmbedder newContentEncoder(HttpMessage msg, String acceptEncoding) + throws Exception { + return new EncoderEmbedder(new OneToOneEncoder() { + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception { + ChannelBuffer in = (ChannelBuffer) msg; + return ChannelBuffers.wrappedBuffer(String.valueOf(in.readableBytes()).getBytes(CharsetUtil.US_ASCII)); + } + }); + } + + @Override + protected String getTargetContentEncoding(String acceptEncoding) throws Exception { + return "test"; + } + } + + static final class HttpContentEncoderEmbedder extends EncoderEmbedder { + HttpContentEncoderEmbedder() { + super(new TestEncoder()); + } + + public void fireMessageReceived(Object msg) { + Channels.fireMessageReceived(getChannel(), msg, null); + } + } + + @Test + public void testSplitContent() throws Exception { + HttpContentEncoderEmbedder e = new HttpContentEncoderEmbedder(); + e.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setChunked(true); + e.offer(res); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[3]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[2]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[1]))); + e.offer(HttpChunk.LAST_CHUNK); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.isChunked(), is(true)); + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("test")); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("3")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("2")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("1")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testChunkedContent() throws Exception { + HttpContentEncoderEmbedder e = new HttpContentEncoderEmbedder(); + e.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED); + e.offer(res); + + assertEncodedResponse(e); + + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[3]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[2]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[1]))); + e.offer(HttpChunk.LAST_CHUNK); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("3")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("2")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("1")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testChunkedContentWithTrailingHeader() throws Exception { + HttpContentEncoderEmbedder e = new HttpContentEncoderEmbedder(); + e.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED); + e.offer(res); + + assertEncodedResponse(e); + + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[3]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[2]))); + e.offer(new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(new byte[1]))); + HttpChunkTrailer trailer = new DefaultHttpChunkTrailer(); + trailer.trailingHeaders().set("X-Test", "Netty"); + e.offer(trailer); + + HttpChunk chunk; + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("3")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("2")); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("1")); + assertThat(chunk, is(instanceOf(HttpChunk.class))); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + assertEquals("Netty", ((HttpChunkTrailer) chunk).trailingHeaders().get("X-Test")); + + assertThat(e.poll(), is(nullValue())); + } + + @Test + public void testFullContent() throws Exception { + HttpContentEncoderEmbedder e = new HttpContentEncoderEmbedder(); + e.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setContent(ChannelBuffers.wrappedBuffer(new byte[42])); + res.headers().set(Names.CONTENT_LENGTH, 42); + e.offer(res); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.isChunked(), is(false)); + assertThat(res.getContent().readableBytes(), is(2)); + assertThat(res.getContent().toString(CharsetUtil.US_ASCII), is("42")); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is("2")); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("test")); + + assertThat(e.poll(), is(nullValue())); + } + + /** + * If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content + * even if the actual length is turned out to be 0. + */ + @Test + public void testEmptySplitContent() throws Exception { + HttpContentEncoderEmbedder e = new HttpContentEncoderEmbedder(); + e.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setChunked(true); + e.offer(res); + + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.isChunked(), is(true)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("test")); + + e.offer(HttpChunk.LAST_CHUNK); + + HttpChunk chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().toString(CharsetUtil.US_ASCII), is("0")); + assertThat(chunk, is(instanceOf(HttpChunk.class))); + + chunk = (HttpChunk) e.poll(); + assertThat(chunk.getContent().readable(), is(false)); + assertThat(chunk, is(instanceOf(HttpChunkTrailer.class))); + + assertThat(e.poll(), is(nullValue())); + } + + /** + * If the length of the content is 0 for sure, {@link HttpContentEncoder} should skip encoding. + */ + @Test + public void testEmptyFullContent() throws Exception { + HttpContentEncoderEmbedder ch = new HttpContentEncoderEmbedder(); + ch.fireMessageReceived(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); + + HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + res.setContent(ChannelBuffers.EMPTY_BUFFER); + ch.offer(res); + + Object o = ch.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = ch.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + res = (HttpResponse) o; + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is(nullValue())); + + // Content encoding shouldn't be modified. + assertThat(res.headers().get(Names.CONTENT_ENCODING), is(nullValue())); + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.getContent().toString(CharsetUtil.US_ASCII), is("")); + + assertThat(ch.poll(), is(nullValue())); + } + + private static void assertEncodedResponse(HttpContentEncoderEmbedder e) { + Object o = e.poll(); + assertThat(o, is(instanceOf(HttpRequest.class))); + + o = e.poll(); + assertThat(o, is(instanceOf(HttpResponse.class))); + + HttpResponse res = (HttpResponse) o; + assertThat(res.getContent().readableBytes(), is(0)); + assertThat(res.headers().get(Names.TRANSFER_ENCODING), is("chunked")); + assertThat(res.headers().get(Names.CONTENT_LENGTH), is(nullValue())); + assertThat(res.headers().get(Names.CONTENT_ENCODING), is("test")); + } +}