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);
|
||||
// Encode the message.
|
||||
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 :
|
||||
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:
|
||||
@ -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
|
||||
* 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);
|
||||
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 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
|
||||
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());
|
||||
}
|
||||
|
||||
@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