Skip to content

Commit 09f141b

Browse files
[8.6] Fix Chunked APIs sending incorrect responses to HEAD requests (#92042) (#92049)
* Fix Chunked APIs sending incorrect responses to HEAD requests (#92042) Response bodies must always be empty for HEAD requests. Since the request encoder does not know that its dealing with a response to a HEAD request we have to indicate this fact to it. Also, needed to adjust the test http client to use the http-codec so it is able to correlate what responses are meant for HEAD requests and will correctly read responses for HEAD requests. Without this change the added test reproduces the extra bytes and fails with an assert about more than one response received. closes #92032 * fix compile
1 parent 8b239b7 commit 09f141b

File tree

4 files changed

+86
-10
lines changed

4 files changed

+86
-10
lines changed

docs/changelog/92042.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 92042
2+
summary: Fix Chunked APIs sending incorrect responses to HEAD requests
3+
area: Network
4+
type: bug
5+
issues:
6+
- 92032

modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import io.netty.handler.codec.http.HttpContentDecompressor;
2525
import io.netty.handler.codec.http.HttpObjectAggregator;
2626
import io.netty.handler.codec.http.HttpRequestDecoder;
27+
import io.netty.handler.codec.http.HttpResponse;
2728
import io.netty.handler.codec.http.HttpResponseEncoder;
29+
import io.netty.handler.codec.http.HttpUtil;
2830
import io.netty.handler.timeout.ReadTimeoutException;
2931
import io.netty.handler.timeout.ReadTimeoutHandler;
3032
import io.netty.util.AttributeKey;
@@ -324,7 +326,18 @@ protected void initChannel(Channel ch) throws Exception {
324326
ch.pipeline()
325327
.addLast("decoder", decoder)
326328
.addLast("decoder_compress", new HttpContentDecompressor())
327-
.addLast("encoder", new HttpResponseEncoder())
329+
.addLast("encoder", new HttpResponseEncoder() {
330+
@Override
331+
protected boolean isContentAlwaysEmpty(HttpResponse msg) {
332+
// non-chunked responses (Netty4HttpResponse extends Netty's DefaultFullHttpResponse) with chunked transfer
333+
// encoding are only sent by us in response to HEAD requests and must always have an empty body
334+
if (msg instanceof Netty4HttpResponse netty4HttpResponse && HttpUtil.isTransferEncodingChunked(msg)) {
335+
assert netty4HttpResponse.content().isReadable() == false;
336+
return true;
337+
}
338+
return super.isContentAlwaysEmpty(msg);
339+
}
340+
})
328341
.addLast("aggregator", aggregator);
329342
if (handlingSettings.compression()) {
330343
ch.pipeline().addLast("encoder_compress", new HttpContentCompressor(handlingSettings.compressionLevel()));

modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
import io.netty.handler.codec.http.DefaultFullHttpRequest;
2222
import io.netty.handler.codec.http.FullHttpRequest;
2323
import io.netty.handler.codec.http.FullHttpResponse;
24+
import io.netty.handler.codec.http.HttpClientCodec;
2425
import io.netty.handler.codec.http.HttpContentDecompressor;
2526
import io.netty.handler.codec.http.HttpHeaderNames;
2627
import io.netty.handler.codec.http.HttpMethod;
2728
import io.netty.handler.codec.http.HttpObject;
2829
import io.netty.handler.codec.http.HttpObjectAggregator;
2930
import io.netty.handler.codec.http.HttpRequest;
30-
import io.netty.handler.codec.http.HttpRequestEncoder;
3131
import io.netty.handler.codec.http.HttpResponse;
32-
import io.netty.handler.codec.http.HttpResponseDecoder;
3332
import io.netty.handler.codec.http.HttpVersion;
3433

3534
import org.elasticsearch.common.unit.ByteSizeUnit;
@@ -175,8 +174,7 @@ private static class CountDownLatchHandler extends ChannelInitializer<SocketChan
175174
@Override
176175
protected void initChannel(SocketChannel ch) {
177176
final int maxContentLength = new ByteSizeValue(100, ByteSizeUnit.MB).bytesAsInt();
178-
ch.pipeline().addLast(new HttpResponseDecoder());
179-
ch.pipeline().addLast(new HttpRequestEncoder());
177+
ch.pipeline().addLast(new HttpClientCodec());
180178
ch.pipeline().addLast(new HttpContentDecompressor());
181179
ch.pipeline().addLast(new HttpObjectAggregator(maxContentLength));
182180
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpObject>() {

modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,14 @@
4040

4141
import org.elasticsearch.ElasticsearchException;
4242
import org.elasticsearch.common.bytes.BytesArray;
43+
import org.elasticsearch.common.collect.Iterators;
4344
import org.elasticsearch.common.network.NetworkAddress;
4445
import org.elasticsearch.common.network.NetworkService;
4546
import org.elasticsearch.common.settings.ClusterSettings;
4647
import org.elasticsearch.common.settings.Setting;
4748
import org.elasticsearch.common.settings.Settings;
4849
import org.elasticsearch.common.transport.TransportAddress;
4950
import org.elasticsearch.common.unit.ByteSizeValue;
50-
import org.elasticsearch.common.util.MockPageCacheRecycler;
51-
import org.elasticsearch.common.util.PageCacheRecycler;
5251
import org.elasticsearch.common.util.concurrent.ThreadContext;
5352
import org.elasticsearch.core.TimeValue;
5453
import org.elasticsearch.http.AbstractHttpServerTransportTestCase;
@@ -57,6 +56,7 @@
5756
import org.elasticsearch.http.HttpServerTransport;
5857
import org.elasticsearch.http.HttpTransportSettings;
5958
import org.elasticsearch.http.NullDispatcher;
59+
import org.elasticsearch.rest.ChunkedRestResponseBody;
6060
import org.elasticsearch.rest.RestChannel;
6161
import org.elasticsearch.rest.RestRequest;
6262
import org.elasticsearch.rest.RestResponse;
@@ -66,6 +66,7 @@
6666
import org.elasticsearch.tracing.Tracer;
6767
import org.elasticsearch.transport.netty4.NettyAllocator;
6868
import org.elasticsearch.transport.netty4.SharedGroupFactory;
69+
import org.elasticsearch.xcontent.ToXContent;
6970
import org.junit.After;
7071
import org.junit.Before;
7172

@@ -94,14 +95,12 @@ public class Netty4HttpServerTransportTests extends AbstractHttpServerTransportT
9495

9596
private NetworkService networkService;
9697
private ThreadPool threadPool;
97-
private PageCacheRecycler recycler;
9898
private ClusterSettings clusterSettings;
9999

100100
@Before
101101
public void setup() throws Exception {
102102
networkService = new NetworkService(Collections.emptyList());
103103
threadPool = new TestThreadPool("test");
104-
recycler = new MockPageCacheRecycler(Settings.EMPTY);
105104
clusterSettings = randomClusterSettings();
106105
}
107106

@@ -112,7 +111,6 @@ public void shutdown() throws Exception {
112111
}
113112
threadPool = null;
114113
networkService = null;
115-
recycler = null;
116114
clusterSettings = null;
117115
}
118116

@@ -560,6 +558,67 @@ protected void initChannel(SocketChannel ch) {
560558
}
561559
}
562560

561+
public void testHeadRequestToChunkedApi() throws InterruptedException {
562+
final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
563+
564+
@Override
565+
public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) {
566+
try {
567+
channel.sendResponse(
568+
new RestResponse(
569+
OK,
570+
ChunkedRestResponseBody.fromXContent(
571+
() -> Iterators.single(
572+
(builder, params) -> { throw new AssertionError("should not be called for HEAD REQUEST"); }
573+
),
574+
ToXContent.EMPTY_PARAMS,
575+
channel
576+
)
577+
)
578+
);
579+
} catch (IOException e) {
580+
throw new AssertionError(e);
581+
}
582+
}
583+
584+
@Override
585+
public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) {
586+
throw new AssertionError();
587+
}
588+
589+
};
590+
591+
final Settings settings = createSettings();
592+
try (
593+
Netty4HttpServerTransport transport = new Netty4HttpServerTransport(
594+
settings,
595+
networkService,
596+
threadPool,
597+
xContentRegistry(),
598+
dispatcher,
599+
clusterSettings,
600+
new SharedGroupFactory(settings),
601+
Tracer.NOOP
602+
)
603+
) {
604+
transport.start();
605+
final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses());
606+
607+
try (Netty4HttpClient client = new Netty4HttpClient()) {
608+
final String url = "/some-head-endpoint";
609+
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, url);
610+
611+
final FullHttpResponse response = client.send(remoteAddress.address(), request);
612+
try {
613+
assertThat(response.status(), equalTo(HttpResponseStatus.OK));
614+
assertFalse(response.content().isReadable());
615+
} finally {
616+
response.release();
617+
}
618+
}
619+
}
620+
}
621+
563622
private Settings createSettings() {
564623
return createBuilderWithPort().build();
565624
}

0 commit comments

Comments
 (0)