Adjust Content-Length header when encoding Full Responses

Motivation:
If a full HttpResponse with a Content-Length header is encoded by the HttpContentEncoder subtypes the Content-Length header is removed and the message is set to Transfer-Encoder: chunked. This is an unnecessary loss of information about the message content.

Modifications:
- If a full HttpResponse has a Content-Length header, the header is adjusted after encoding.

Result:
Complete messages continue to have the Content-Length header after encoding.
This commit is contained in:
Bryce Anderson 2017-06-02 14:54:09 -06:00 committed by Norman Maurer
parent f8788a9f6c
commit 9fa3e556f3
3 changed files with 96 additions and 10 deletions

View File

@ -164,18 +164,21 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
// so that the message looks like a decoded message. // so that the message looks like a decoded message.
res.headers().set(HttpHeaderNames.CONTENT_ENCODING, result.targetContentEncoding()); res.headers().set(HttpHeaderNames.CONTENT_ENCODING, result.targetContentEncoding());
// Make the response chunked to simplify content transformation.
res.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
// Output the rewritten response. // Output the rewritten response.
if (isFull) { if (isFull) {
// Convert full message into unfull one. // Convert full message into unfull one.
HttpResponse newRes = new DefaultHttpResponse(res.protocolVersion(), res.status()); HttpResponse newRes = new DefaultHttpResponse(res.protocolVersion(), res.status());
newRes.headers().set(res.headers()); newRes.headers().set(res.headers());
out.add(newRes); out.add(newRes);
// Fall through to encode the content of the full response.
ensureContent(res);
encodeFullResponse(newRes, (HttpContent) res, out);
break;
} else { } else {
// Make the response chunked to simplify content transformation.
res.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
out.add(res); out.add(res);
state = State.AWAIT_CONTENT; state = State.AWAIT_CONTENT;
if (!(msg instanceof HttpContent)) { if (!(msg instanceof HttpContent)) {
@ -205,6 +208,25 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
} }
} }
private void encodeFullResponse(HttpResponse newRes, HttpContent content, List<Object> out) {
int existingMessages = out.size();
encodeContent(content, out);
if (HttpUtil.isContentLengthSet(newRes)) {
// adjust the content-length header
int messageSize = 0;
for (int i = existingMessages; i < out.size(); i++) {
Object item = out.get(i);
if (item instanceof HttpContent) {
messageSize += ((HttpContent) item).content().readableBytes();
}
}
HttpUtil.setContentLength(newRes, messageSize);
} else {
newRes.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
}
private static boolean isPassthru(HttpVersion version, int code, CharSequence httpMethod) { private static boolean isPassthru(HttpVersion version, int code, CharSequence httpMethod) {
return code < 200 || code == 204 || code == 304 || return code < 200 || code == 204 || code == 304 ||
(httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200)) || (httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200)) ||

View File

@ -197,15 +197,52 @@ public class HttpContentCompressorTest {
assertThat(ch.readOutbound(), is(nullValue())); assertThat(ch.readOutbound(), is(nullValue()));
} }
@Test
public void testFullContentWithContentLength() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
ch.writeInbound(newRequest());
FullHttpResponse fullRes = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII));
fullRes.headers().set(HttpHeaderNames.CONTENT_LENGTH, fullRes.content().readableBytes());
ch.writeOutbound(fullRes);
HttpResponse res = ch.readOutbound();
assertThat(res, is(not(instanceOf(HttpContent.class))));
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is(nullValue()));
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("gzip"));
long contentLengthHeaderValue = HttpUtil.getContentLength(res);
long observedLength = 0;
HttpContent c = ch.readOutbound();
observedLength += c.content().readableBytes();
assertThat(ByteBufUtil.hexDump(c.content()), is("1f8b0800000000000000f248cdc9c9d75108cf2fca4901000000ffff"));
c.release();
c = ch.readOutbound();
observedLength += c.content().readableBytes();
assertThat(ByteBufUtil.hexDump(c.content()), is("0300c6865b260c000000"));
c.release();
LastHttpContent last = ch.readOutbound();
assertThat(last.content().readableBytes(), is(0));
last.release();
assertThat(ch.readOutbound(), is(nullValue()));
assertEquals(contentLengthHeaderValue, observedLength);
}
@Test @Test
public void testFullContent() throws Exception { public void testFullContent() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor()); EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
ch.writeInbound(newRequest()); ch.writeInbound(newRequest());
FullHttpResponse res = new DefaultFullHttpResponse( FullHttpResponse res = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII)); Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII));
res.headers().set(HttpHeaderNames.CONTENT_LENGTH, res.content().readableBytes());
ch.writeOutbound(res); ch.writeOutbound(res);
assertEncodedResponse(ch); assertEncodedResponse(ch);

View File

@ -160,14 +160,41 @@ public class HttpContentEncoderTest {
assertThat(ch.readOutbound(), is(nullValue())); assertThat(ch.readOutbound(), is(nullValue()));
} }
@Test
public void testFullContentWithContentLength() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new TestEncoder());
ch.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"));
FullHttpResponse fullRes = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(new byte[42]));
fullRes.headers().set(HttpHeaderNames.CONTENT_LENGTH, 42);
ch.writeOutbound(fullRes);
HttpResponse res = ch.readOutbound();
assertThat(res, is(not(instanceOf(HttpContent.class))));
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is(nullValue()));
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is("2"));
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("test"));
HttpContent c = ch.readOutbound();
assertThat(c.content().readableBytes(), is(2));
assertThat(c.content().toString(CharsetUtil.US_ASCII), is("42"));
c.release();
LastHttpContent last = ch.readOutbound();
assertThat(last.content().readableBytes(), is(0));
last.release();
assertThat(ch.readOutbound(), is(nullValue()));
}
@Test @Test
public void testFullContent() throws Exception { public void testFullContent() throws Exception {
EmbeddedChannel ch = new EmbeddedChannel(new TestEncoder()); EmbeddedChannel ch = new EmbeddedChannel(new TestEncoder());
ch.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); ch.writeInbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"));
FullHttpResponse res = new DefaultFullHttpResponse( FullHttpResponse res = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(new byte[42])); HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(new byte[42]));
res.headers().set(HttpHeaderNames.CONTENT_LENGTH, 42);
ch.writeOutbound(res); ch.writeOutbound(res);
assertEncodedResponse(ch); assertEncodedResponse(ch);