Correctly not write any body when 1xx, 204 or 304 is used as response status code.

Motivation:

We need to ensure we not write any body when a response with status code of 1xx, 204 or 304 is used as stated in rfc:
https://tools.ietf.org/html/rfc7230#section-3.3.3

Modifications:

- Correctly handle status codes
- Add unit tests

Result:

Correctly handle responses with 1xx, 204, 304 status codes.
This commit is contained in:
Norman Maurer 2017-09-13 15:12:25 -07:00
parent 4d5f0e7ad5
commit 70c5c48eab
4 changed files with 176 additions and 5 deletions

View File

@ -92,12 +92,16 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
buf = ctx.alloc().buffer((int) headersEncodedSizeAccumulator); buf = ctx.alloc().buffer((int) headersEncodedSizeAccumulator);
// Encode the message. // Encode the message.
encodeInitialLine(buf, m); encodeInitialLine(buf, m);
encodeHeaders(m.headers(), buf);
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY : state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY :
HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK; HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);
encodeHeaders(m.headers(), buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
} }
// Bypass the encoder in case of an empty buffer, so that the following idiom works: // Bypass the encoder in case of an empty buffer, so that the following idiom works:
@ -210,6 +214,13 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
} }
} }
/**
* Allows to sanitize headers of the message before encoding these.
*/
protected void sanitizeHeadersBeforeEncode(@SuppressWarnings("unused") H msg, boolean isAlwaysEmpty) {
// noop
}
/** /**
* Determine whether a message has a content or not. Some message may have headers indicating * Determine whether a message has a content or not. Some message may have headers indicating
* a content without having an actual content, e.g the response to an HEAD or CONNECT request. * a content without having an actual content, e.g the response to an HEAD or CONNECT request.

View File

@ -38,4 +38,42 @@ public class HttpResponseEncoder extends HttpObjectEncoder<HttpResponse> {
response.status().encode(buf); response.status().encode(buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT); ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
} }
@Override
protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) {
if (isAlwaysEmpty) {
HttpResponseStatus status = msg.status();
if (status.codeClass() == HttpStatusClass.INFORMATIONAL ||
status.code() == HttpResponseStatus.NO_CONTENT.code()) {
// Stripping Content-Length:
// See https://tools.ietf.org/html/rfc7230#section-3.3.2
msg.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
// Stripping Transfer-Encoding:
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
}
}
}
@Override
protected boolean isContentAlwaysEmpty(HttpResponse msg) {
// Correctly handle special cases as stated in:
// https://tools.ietf.org/html/rfc7230#section-3.3.3
HttpResponseStatus status = msg.status();
if (status.codeClass() == HttpStatusClass.INFORMATIONAL) {
if (status.code() == HttpResponseStatus.SWITCHING_PROTOCOLS.code()) {
// We need special handling for WebSockets version 00 as it will include an body.
// Fortunally this version should not really be used in the wild very often.
// See https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00#section-1.2
return msg.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_VERSION);
}
return true;
}
return status.code() == HttpResponseStatus.NO_CONTENT.code() ||
status.code() == HttpResponseStatus.NOT_MODIFIED.code();
}
} }

View File

@ -111,9 +111,24 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ
private final class HttpServerResponseEncoder extends HttpResponseEncoder { private final class HttpServerResponseEncoder extends HttpResponseEncoder {
private HttpMethod method;
@Override
protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) {
if (!isAlwaysEmpty && method == HttpMethod.CONNECT && msg.status().codeClass() == HttpStatusClass.SUCCESS) {
// Stripping Transfer-Encoding:
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
return;
}
super.sanitizeHeadersBeforeEncode(msg, isAlwaysEmpty);
}
@Override @Override
protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) { protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) {
return HttpMethod.HEAD.equals(queue.poll()); method = queue.poll();
return HttpMethod.HEAD.equals(method) || super.isContentAlwaysEmpty(msg);
} }
} }
} }

View File

@ -195,4 +195,111 @@ public class HttpResponseEncoderTest {
assertFalse(channel.finish()); assertFalse(channel.finish());
} }
@Test
public void testStatusNoContent() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NO_CONTENT, null, false);
assertFalse(channel.finish());
}
@Test
public void testStatusNoContentContentLength() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NO_CONTENT, HttpHeaderNames.CONTENT_LENGTH, true);
assertFalse(channel.finish());
}
@Test
public void testStatusNoContentTransferEncoding() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NO_CONTENT, HttpHeaderNames.TRANSFER_ENCODING, true);
assertFalse(channel.finish());
}
@Test
public void testStatusNotModified() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NOT_MODIFIED, null, false);
assertFalse(channel.finish());
}
@Test
public void testStatusNotModifiedContentLength() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NOT_MODIFIED, HttpHeaderNames.CONTENT_LENGTH, false);
assertFalse(channel.finish());
}
@Test
public void testStatusNotModifiedTransferEncoding() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
assertEmptyResponse(channel, HttpResponseStatus.NOT_MODIFIED, HttpHeaderNames.TRANSFER_ENCODING, false);
assertFalse(channel.finish());
}
@Test
public void testStatusInformational() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
for (int code = 100; code < 200; code++) {
HttpResponseStatus status = HttpResponseStatus.valueOf(code);
assertEmptyResponse(channel, status, null, false);
}
assertFalse(channel.finish());
}
@Test
public void testStatusInformationalContentLength() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
for (int code = 100; code < 200; code++) {
HttpResponseStatus status = HttpResponseStatus.valueOf(code);
assertEmptyResponse(channel, status, HttpHeaderNames.CONTENT_LENGTH, code != 101);
}
assertFalse(channel.finish());
}
@Test
public void testStatusInformationalTransferEncoding() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
for (int code = 100; code < 200; code++) {
HttpResponseStatus status = HttpResponseStatus.valueOf(code);
assertEmptyResponse(channel, status, HttpHeaderNames.TRANSFER_ENCODING, code != 101);
}
assertFalse(channel.finish());
}
private static void assertEmptyResponse(EmbeddedChannel channel, HttpResponseStatus status,
CharSequence headerName, boolean headerStripped) {
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
if (HttpHeaderNames.CONTENT_LENGTH.contentEquals(headerName)) {
response.headers().set(headerName, "0");
} else if (HttpHeaderNames.TRANSFER_ENCODING.contentEquals(headerName)) {
response.headers().set(headerName, HttpHeaderValues.CHUNKED);
}
assertTrue(channel.writeOutbound(response));
assertTrue(channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT));
ByteBuf buffer = channel.readOutbound();
StringBuilder responseText = new StringBuilder();
responseText.append(HttpVersion.HTTP_1_1.toString()).append(' ').append(status.toString()).append("\r\n");
if (!headerStripped && headerName != null) {
responseText.append(headerName).append(": ");
if (HttpHeaderNames.CONTENT_LENGTH.contentEquals(headerName)) {
responseText.append('0');
} else {
responseText.append(HttpHeaderValues.CHUNKED.toString());
}
responseText.append("\r\n");
}
responseText.append("\r\n");
assertEquals(responseText.toString(), buffer.toString(CharsetUtil.US_ASCII));
buffer.release();
buffer = channel.readOutbound();
buffer.release();
}
} }