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:
parent
4d5f0e7ad5
commit
70c5c48eab
@ -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.
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user