[#4079] Fix IllegalStateException when HttpContentEncoder is used and 100 Continue response is used.

Motivation:

Whe a 100 Continue response was written an IllegalStateException was produced as soon as the user wrote the following response. This regression was introduced by 41b0080fcc.

Modifications:

- Special handle 100 Continue responses
- Added unit tests

Result:

Fixed regression.
This commit is contained in:
Norman Maurer 2015-08-13 09:56:15 +02:00
parent 9a445206ca
commit 16d136dc55
2 changed files with 90 additions and 10 deletions

View File

@ -60,6 +60,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
private static final CharSequence ZERO_LENGTH_HEAD = "HEAD";
private static final CharSequence ZERO_LENGTH_CONNECT = "CONNECT";
private static final int CONTINUE_CODE = HttpResponseStatus.CONTINUE.code();
private final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
private CharSequence acceptEncoding;
@ -99,11 +100,17 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
assert encoder == null;
final HttpResponse res = (HttpResponse) msg;
// Get the list of encodings accepted by the peer.
acceptEncoding = acceptEncodingQueue.poll();
if (acceptEncoding == null) {
throw new IllegalStateException("cannot send more responses than requests");
final int code = res.getStatus().code();
if (code == CONTINUE_CODE) {
// We need to not poll the encoding when response with CONTINUE as another response will follow
// for the issued request. See https://github.com/netty/netty/issues/4079
acceptEncoding = null;
} else {
// Get the list of encodings accepted by the peer.
acceptEncoding = acceptEncodingQueue.poll();
if (acceptEncoding == null) {
throw new IllegalStateException("cannot send more responses than requests");
}
}
/*
@ -117,7 +124,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
*
* This code is now inline with HttpClientDecoder.Decoder
*/
if (isPassthru(res, acceptEncoding)) {
if (isPassthru(code, acceptEncoding)) {
if (isFull) {
out.add(ReferenceCountUtil.retain(res));
} else {
@ -198,10 +205,9 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
}
}
private static boolean isPassthru(HttpResponse res, CharSequence httpMethod) {
final int code = res.getStatus().code();
boolean expectEmptyBody = httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200);
return code < 200 || code == 204 || code == 304 || expectEmptyBody;
private static boolean isPassthru(int code, CharSequence httpMethod) {
return code < 200 || code == 204 || code == 304 ||
(httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200));
}
private static void ensureHeaders(HttpObject msg) {

View File

@ -18,10 +18,12 @@ package io.netty.handler.codec.http;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.http.HttpHeaders.Names;
import io.netty.handler.codec.http.HttpHeaders.Values;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
@ -292,6 +294,78 @@ public class HttpContentCompressorTest {
assertThat(ch.readOutbound(), is(nullValue()));
}
@Test
public void test100Continue() throws Exception {
FullHttpRequest request = newRequest();
HttpHeaders.set100ContinueExpected(request);
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
ch.writeInbound(request);
FullHttpResponse continueResponse = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE, Unpooled.EMPTY_BUFFER);
ch.writeOutbound(continueResponse);
FullHttpResponse res = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
res.trailingHeaders().set("X-Test", "Netty");
ch.writeOutbound(res);
Object o = ch.readOutbound();
assertThat(o, is(instanceOf(FullHttpResponse.class)));
res = (FullHttpResponse) o;
assertSame(continueResponse, res);
res.release();
o = ch.readOutbound();
assertThat(o, is(instanceOf(FullHttpResponse.class)));
res = (FullHttpResponse) 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.content().readableBytes(), is(0));
assertThat(res.content().toString(CharsetUtil.US_ASCII), is(""));
assertEquals("Netty", res.trailingHeaders().get("X-Test"));
assertThat(ch.readOutbound(), is(nullValue()));
}
@Test
public void testTooManyResponses() throws Exception {
FullHttpRequest request = newRequest();
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
ch.writeInbound(request);
ch.writeOutbound(new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER));
try {
ch.writeOutbound(new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER));
fail();
} catch (EncoderException e) {
assertTrue(e.getCause() instanceof IllegalStateException);
}
assertTrue(ch.finish());
for (;;) {
Object message = ch.readOutbound();
if (message == null) {
break;
}
ReferenceCountUtil.release(message);
}
for (;;) {
Object message = ch.readInbound();
if (message == null) {
break;
}
ReferenceCountUtil.release(message);
}
}
private static FullHttpRequest newRequest() {
FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
req.headers().set(Names.ACCEPT_ENCODING, "gzip");