Skip to content

Commit bc9e32b

Browse files
authored
Fix lost headers with chunked responses (#104845)
* Fix lost headers with chunked responses In #99852 we introduced a layer of wrapping around the `RestResponse` to capture the length of a chunked-encoded response, but this wrapping does not preserve the headers of the original response. This commit fixes the bug. Backport of #104808 to 8.12 * Fix compile
1 parent 2f74248 commit bc9e32b

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

docs/changelog/104808.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 104808
2+
summary: Fix lost headers with chunked responses
3+
area: Network
4+
type: bug
5+
issues: []
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.rest;
10+
11+
import org.elasticsearch.client.Request;
12+
import org.elasticsearch.client.internal.node.NodeClient;
13+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
14+
import org.elasticsearch.cluster.node.DiscoveryNodes;
15+
import org.elasticsearch.common.collect.Iterators;
16+
import org.elasticsearch.common.settings.ClusterSettings;
17+
import org.elasticsearch.common.settings.IndexScopedSettings;
18+
import org.elasticsearch.common.settings.Settings;
19+
import org.elasticsearch.common.settings.SettingsFilter;
20+
import org.elasticsearch.common.util.CollectionUtils;
21+
import org.elasticsearch.plugins.ActionPlugin;
22+
import org.elasticsearch.plugins.Plugin;
23+
import org.elasticsearch.test.ESIntegTestCase;
24+
25+
import java.io.IOException;
26+
import java.util.Collection;
27+
import java.util.List;
28+
import java.util.function.Supplier;
29+
30+
public class RestControllerIT extends ESIntegTestCase {
31+
@Override
32+
protected boolean addMockHttpTransport() {
33+
return false; // enable HTTP
34+
}
35+
36+
public void testHeadersEmittedWithChunkedResponses() throws IOException {
37+
final var client = getRestClient();
38+
final var response = client.performRequest(new Request("GET", ChunkedResponseWithHeadersPlugin.ROUTE));
39+
assertEquals(200, response.getStatusLine().getStatusCode());
40+
assertEquals(ChunkedResponseWithHeadersPlugin.HEADER_VALUE, response.getHeader(ChunkedResponseWithHeadersPlugin.HEADER_NAME));
41+
}
42+
43+
@Override
44+
protected Collection<Class<? extends Plugin>> nodePlugins() {
45+
return CollectionUtils.appendToCopy(super.nodePlugins(), ChunkedResponseWithHeadersPlugin.class);
46+
}
47+
48+
public static class ChunkedResponseWithHeadersPlugin extends Plugin implements ActionPlugin {
49+
50+
static final String ROUTE = "/_test/chunked_response_with_headers";
51+
static final String HEADER_NAME = "test-header";
52+
static final String HEADER_VALUE = "test-header-value";
53+
54+
@Override
55+
public List<RestHandler> getRestHandlers(
56+
Settings settings,
57+
RestController restController,
58+
ClusterSettings clusterSettings,
59+
IndexScopedSettings indexScopedSettings,
60+
SettingsFilter settingsFilter,
61+
IndexNameExpressionResolver indexNameExpressionResolver,
62+
Supplier<DiscoveryNodes> nodesInCluster
63+
) {
64+
return List.of(new BaseRestHandler() {
65+
@Override
66+
public String getName() {
67+
return ChunkedResponseWithHeadersPlugin.class.getCanonicalName();
68+
}
69+
70+
@Override
71+
public List<Route> routes() {
72+
return List.of(new Route(RestRequest.Method.GET, ROUTE));
73+
}
74+
75+
@Override
76+
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) {
77+
return channel -> {
78+
final var response = RestResponse.chunked(
79+
RestStatus.OK,
80+
ChunkedRestResponseBody.fromXContent(
81+
params -> Iterators.single((b, p) -> b.startObject().endObject()),
82+
request,
83+
channel,
84+
null
85+
)
86+
);
87+
response.addHeader(HEADER_NAME, HEADER_VALUE);
88+
channel.sendResponse(response);
89+
};
90+
}
91+
});
92+
}
93+
}
94+
}

server/src/main/java/org/elasticsearch/rest/RestController.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,10 +791,14 @@ public void sendResponse(RestResponse response) {
791791
if (response.isChunked() == false) {
792792
methodHandlers.addResponseStats(response.content().length());
793793
} else {
794-
response = RestResponse.chunked(
795-
response.status(),
796-
new EncodedLengthTrackingChunkedRestResponseBody(response.chunkedContent(), methodHandlers)
797-
);
794+
final var wrapped = new EncodedLengthTrackingChunkedRestResponseBody(response.chunkedContent(), methodHandlers);
795+
final var headers = response.getHeaders();
796+
response = RestResponse.chunked(response.status(), wrapped);
797+
for (final var header : headers.entrySet()) {
798+
for (final var value : header.getValue()) {
799+
response.addHeader(header.getKey(), value);
800+
}
801+
}
798802
}
799803
delegate.sendResponse(response);
800804
success = true;

0 commit comments

Comments
 (0)