Skip to content

Commit de8a7ef

Browse files
authored
Implement Early hints response status, do not poll the encoding with HTTP 1xx codes (#12918)
Motivation: Http 1xx codes should expect another response next Modifications: Replace code == 100 with code == 1xx Result: Fixes #12904
1 parent e1437fb commit de8a7ef

File tree

7 files changed

+169
-14
lines changed

7 files changed

+169
-14
lines changed

codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,10 @@ protected boolean isContentAlwaysEmpty(HttpMessage msg) {
266266
// request / response pairs in sync.
267267
HttpMethod method = queue.poll();
268268

269-
final int statusCode = ((HttpResponse) msg).status().code();
270-
if (statusCode >= 100 && statusCode < 200) {
269+
final HttpResponseStatus status = ((HttpResponse) msg).status();
270+
final HttpStatusClass statusClass = status.codeClass();
271+
final int statusCode = status.code();
272+
if (statusClass == HttpStatusClass.INFORMATIONAL) {
271273
// An informational response should be excluded from paired comparison.
272274
// Just delegate to super method which has all the needed handling.
273275
return super.isContentAlwaysEmpty(msg);

codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ private enum State {
6363

6464
private static final CharSequence ZERO_LENGTH_HEAD = "HEAD";
6565
private static final CharSequence ZERO_LENGTH_CONNECT = "CONNECT";
66-
private static final int CONTINUE_CODE = HttpResponseStatus.CONTINUE.code();
6766

6867
private final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
6968
private EmbeddedChannel encoder;
@@ -112,10 +111,12 @@ protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> ou
112111

113112
final HttpResponse res = (HttpResponse) msg;
114113
final int code = res.status().code();
114+
final HttpStatusClass codeClass = res.status().codeClass();
115115
final CharSequence acceptEncoding;
116-
if (code == CONTINUE_CODE) {
117-
// We need to not poll the encoding when response with CONTINUE as another response will follow
118-
// for the issued request. See https://github.com/netty/netty/issues/4079
116+
if (codeClass == HttpStatusClass.INFORMATIONAL) {
117+
// We need to not poll the encoding when response with 1xx codes as another response will follow
118+
// for the issued request.
119+
// See https://github.com/netty/netty/issues/12904 and https://github.com/netty/netty/issues/4079
119120
acceptEncoding = null;
120121
} else {
121122
// Get the list of encodings accepted by the peer.

codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,14 +522,16 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
522522
protected boolean isContentAlwaysEmpty(HttpMessage msg) {
523523
if (msg instanceof HttpResponse) {
524524
HttpResponse res = (HttpResponse) msg;
525-
int code = res.status().code();
525+
final HttpResponseStatus status = res.status();
526+
final int code = status.code();
527+
final HttpStatusClass statusClass = status.codeClass();
526528

527529
// Correctly handle return codes of 1xx.
528530
//
529531
// See:
530532
// - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
531533
// - https://github.com/netty/netty/issues/222
532-
if (code >= 100 && code < 200) {
534+
if (statusClass == HttpStatusClass.INFORMATIONAL) {
533535
// One exception: Hixie 76 websocket handshake response
534536
return !(code == 101 && !res.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT)
535537
&& res.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true));

codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
4848
*/
4949
public static final HttpResponseStatus PROCESSING = newStatus(102, "Processing");
5050

51+
/**
52+
* 103 Early Hints (RFC 8297)
53+
*/
54+
public static final HttpResponseStatus EARLY_HINTS = newStatus(103, "Early Hints");
55+
5156
/**
5257
* 200 OK
5358
*/
@@ -344,6 +349,8 @@ private static HttpResponseStatus valueOf0(int code) {
344349
return SWITCHING_PROTOCOLS;
345350
case 102:
346351
return PROCESSING;
352+
case 103:
353+
return EARLY_HINTS;
347354
case 200:
348355
return OK;
349356
case 201:

codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,96 @@ public void test100Continue() throws Exception {
603603
assertThat(ch.readOutbound(), is(nullValue()));
604604
}
605605

606+
@Test
607+
public void testMultiple1xxInformationalResponse() throws Exception {
608+
FullHttpRequest request = newRequest();
609+
HttpUtil.set100ContinueExpected(request, true);
610+
611+
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
612+
ch.writeInbound(request);
613+
614+
FullHttpResponse continueResponse = new DefaultFullHttpResponse(
615+
HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE, Unpooled.EMPTY_BUFFER);
616+
ch.writeOutbound(continueResponse);
617+
618+
FullHttpResponse earlyHintsResponse = new DefaultFullHttpResponse(
619+
HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS, Unpooled.EMPTY_BUFFER);
620+
earlyHintsResponse.trailingHeaders().set(of("X-Test"), of("Netty"));
621+
ch.writeOutbound(earlyHintsResponse);
622+
623+
FullHttpResponse res = new DefaultFullHttpResponse(
624+
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
625+
res.trailingHeaders().set(of("X-Test"), of("Netty"));
626+
ch.writeOutbound(res);
627+
628+
Object o = ch.readOutbound();
629+
assertThat(o, is(instanceOf(FullHttpResponse.class)));
630+
631+
res = (FullHttpResponse) o;
632+
assertSame(continueResponse, res);
633+
res.release();
634+
635+
o = ch.readOutbound();
636+
assertThat(o, is(instanceOf(FullHttpResponse.class)));
637+
638+
res = (FullHttpResponse) o;
639+
assertSame(earlyHintsResponse, res);
640+
res.release();
641+
642+
o = ch.readOutbound();
643+
assertThat(o, is(instanceOf(FullHttpResponse.class)));
644+
645+
res = (FullHttpResponse) o;
646+
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is(nullValue()));
647+
648+
// Content encoding shouldn't be modified.
649+
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is(nullValue()));
650+
assertThat(res.content().readableBytes(), is(0));
651+
assertThat(res.content().toString(CharsetUtil.US_ASCII), is(""));
652+
assertEquals("Netty", res.trailingHeaders().get(of("X-Test")));
653+
assertEquals(DecoderResult.SUCCESS, res.decoderResult());
654+
assertThat(ch.readOutbound(), is(nullValue()));
655+
}
656+
657+
@Test
658+
public void test103EarlyHintsResponse() throws Exception {
659+
FullHttpRequest request = newRequest();
660+
661+
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
662+
ch.writeInbound(request);
663+
664+
FullHttpResponse earlyHintsResponse = new DefaultFullHttpResponse(
665+
HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS, Unpooled.EMPTY_BUFFER);
666+
earlyHintsResponse.trailingHeaders().set(of("X-Test"), of("Netty"));
667+
ch.writeOutbound(earlyHintsResponse);
668+
669+
FullHttpResponse res = new DefaultFullHttpResponse(
670+
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
671+
res.trailingHeaders().set(of("X-Test"), of("Netty"));
672+
ch.writeOutbound(res);
673+
674+
Object o = ch.readOutbound();
675+
assertThat(o, is(instanceOf(FullHttpResponse.class)));
676+
677+
res = (FullHttpResponse) o;
678+
assertSame(earlyHintsResponse, res);
679+
res.release();
680+
681+
o = ch.readOutbound();
682+
assertThat(o, is(instanceOf(FullHttpResponse.class)));
683+
684+
res = (FullHttpResponse) o;
685+
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is(nullValue()));
686+
687+
// Content encoding shouldn't be modified.
688+
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is(nullValue()));
689+
assertThat(res.content().readableBytes(), is(0));
690+
assertThat(res.content().toString(CharsetUtil.US_ASCII), is(""));
691+
assertEquals("Netty", res.trailingHeaders().get(of("X-Test")));
692+
assertEquals(DecoderResult.SUCCESS, res.decoderResult());
693+
assertThat(ch.readOutbound(), is(nullValue()));
694+
}
695+
606696
@Test
607697
public void testTooManyResponses() throws Exception {
608698
FullHttpRequest request = newRequest();

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.netty.handler.codec.http.HttpResponse;
3838
import io.netty.handler.codec.http.HttpResponseStatus;
3939
import io.netty.handler.codec.http.HttpScheme;
40+
import io.netty.handler.codec.http.HttpStatusClass;
4041
import io.netty.handler.codec.http.HttpUtil;
4142
import io.netty.handler.codec.http.HttpVersion;
4243
import io.netty.handler.codec.http.LastHttpContent;
@@ -91,9 +92,9 @@ protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame, List<Ob
9192

9293
final CharSequence status = headers.status();
9394

94-
// 100-continue response is a special case where Http2HeadersFrame#isEndStream=false
95+
// 1xx response (excluding 101) is a special case where Http2HeadersFrame#isEndStream=false
9596
// but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator.
96-
if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) {
97+
if (null != status && isInformationalResponseHeaderFrame(status)) {
9798
final FullHttpMessage fullMsg = newFullMessage(id, headers, ctx.alloc());
9899
out.add(fullMsg);
99100
return;
@@ -151,18 +152,22 @@ private void encodeLastContent(LastHttpContent last, List<Object> out) {
151152
*/
152153
@Override
153154
protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> out) throws Exception {
154-
// 100-continue is typically a FullHttpResponse, but the decoded
155+
// 1xx (excluding 101) is typically a FullHttpResponse, but the decoded
155156
// Http2HeadersFrame should not be marked as endStream=true
156157
if (obj instanceof HttpResponse) {
157158
final HttpResponse res = (HttpResponse) obj;
158-
if (res.status().equals(HttpResponseStatus.CONTINUE)) {
159+
final HttpResponseStatus status = res.status();
160+
final int code = status.code();
161+
final HttpStatusClass statusClass = status.codeClass();
162+
// An informational response using a 1xx status code other than 101 is
163+
// transmitted as a HEADERS frame
164+
if (statusClass == HttpStatusClass.INFORMATIONAL && code != 101) {
159165
if (res instanceof FullHttpResponse) {
160166
final Http2Headers headers = toHttp2Headers(ctx, res);
161167
out.add(new DefaultHttp2HeadersFrame(headers, false));
162168
return;
163169
} else {
164-
throw new EncoderException(
165-
HttpResponseStatus.CONTINUE + " must be a FullHttpResponse");
170+
throw new EncoderException(status + " must be a FullHttpResponse");
166171
}
167172
}
168173
}
@@ -247,4 +252,20 @@ private static Channel connectionChannel(ChannelHandlerContext ctx) {
247252
final Channel ch = ctx.channel();
248253
return ch instanceof Http2StreamChannel ? ch.parent() : ch;
249254
}
255+
256+
/**
257+
* An informational response using a 1xx status code other than 101 is
258+
* transmitted as a HEADERS frame
259+
*/
260+
private static boolean isInformationalResponseHeaderFrame(CharSequence status) {
261+
if (status.length() == 3) {
262+
char char0 = status.charAt(0);
263+
char char1 = status.charAt(1);
264+
char char2 = status.charAt(2);
265+
return char0 == '1'
266+
&& char1 >= '0' && char1 <= '9'
267+
&& char2 >= '0' && char2 <= '9' && char2 != '1';
268+
}
269+
return false;
270+
}
250271
}

codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodecTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,38 @@ public void decode100ContinueHttp2HeadersAsFullHttpResponse() throws Exception {
708708
assertFalse(ch.finish());
709709
}
710710

711+
/**
712+
* An informational response using a 1xx status code other than 101 is
713+
* transmitted as a HEADERS frame, followed by zero or more CONTINUATION
714+
* frames.
715+
* Trailing header fields are sent as a header block after both the
716+
* request or response header block and all the DATA frames have been
717+
* sent. The HEADERS frame starting the trailers header block has the
718+
* END_STREAM flag set.
719+
*/
720+
@Test
721+
public void decode103EarlyHintsHttp2HeadersAsFullHttpResponse() throws Exception {
722+
EmbeddedChannel ch = new EmbeddedChannel(new Http2StreamFrameToHttpObjectCodec(false));
723+
Http2Headers headers = new DefaultHttp2Headers();
724+
headers.scheme(HttpScheme.HTTP.name());
725+
headers.status(HttpResponseStatus.EARLY_HINTS.codeAsText());
726+
headers.set("key", "value");
727+
728+
assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers, false)));
729+
730+
final FullHttpResponse response = ch.readInbound();
731+
try {
732+
assertThat(response.status(), is(HttpResponseStatus.EARLY_HINTS));
733+
assertThat(response.protocolVersion(), is(HttpVersion.HTTP_1_1));
734+
assertThat(response.headers().get("key"), is("value"));
735+
} finally {
736+
response.release();
737+
}
738+
739+
assertThat(ch.readInbound(), is(nullValue()));
740+
assertFalse(ch.finish());
741+
}
742+
711743
@Test
712744
public void testDecodeResponseHeaders() throws Exception {
713745
EmbeddedChannel ch = new EmbeddedChannel(new Http2StreamFrameToHttpObjectCodec(false));

0 commit comments

Comments
 (0)