10
10
package org .elasticsearch .rest ;
11
11
12
12
import org .elasticsearch .client .Request ;
13
+ import org .elasticsearch .client .RequestOptions ;
13
14
import org .elasticsearch .client .ResponseException ;
14
15
import org .elasticsearch .client .internal .node .NodeClient ;
15
16
import org .elasticsearch .cluster .metadata .IndexNameExpressionResolver ;
20
21
import org .elasticsearch .common .settings .IndexScopedSettings ;
21
22
import org .elasticsearch .common .settings .Settings ;
22
23
import org .elasticsearch .common .settings .SettingsFilter ;
24
+ import org .elasticsearch .common .xcontent .ChunkedToXContentHelper ;
23
25
import org .elasticsearch .features .NodeFeature ;
24
26
import org .elasticsearch .logging .LogManager ;
25
27
import org .elasticsearch .logging .Logger ;
30
32
import org .elasticsearch .telemetry .Measurement ;
31
33
import org .elasticsearch .telemetry .TestTelemetryPlugin ;
32
34
import org .elasticsearch .test .ESIntegTestCase ;
35
+ import org .elasticsearch .xcontent .XContentParser ;
33
36
34
37
import java .io .IOException ;
35
38
import java .util .ArrayList ;
36
39
import java .util .Collection ;
40
+ import java .util .HashMap ;
37
41
import java .util .List ;
38
42
import java .util .function .Consumer ;
39
43
import java .util .function .Predicate ;
40
44
import java .util .function .Supplier ;
41
45
46
+ import static org .elasticsearch .test .rest .ESRestTestCase .responseAsParser ;
47
+ import static org .hamcrest .Matchers .containsInAnyOrder ;
48
+ import static org .hamcrest .Matchers .containsString ;
49
+ import static org .hamcrest .Matchers .equalTo ;
42
50
import static org .hamcrest .Matchers .hasEntry ;
43
51
import static org .hamcrest .Matchers .hasSize ;
44
52
import static org .hamcrest .Matchers .instanceOf ;
@@ -58,6 +66,49 @@ public void testHeadersEmittedWithChunkedResponses() throws IOException {
58
66
assertEquals (ChunkedResponseWithHeadersPlugin .HEADER_VALUE , response .getHeader (ChunkedResponseWithHeadersPlugin .HEADER_NAME ));
59
67
}
60
68
69
+ public void testHeadersAreCollapsed () throws IOException {
70
+ final var client = getRestClient ();
71
+ final var request = new Request ("GET" , TestEchoHeadersPlugin .ROUTE );
72
+ request .setOptions (RequestOptions .DEFAULT .toBuilder ().addHeader ("X-Foo" , "1" ).addHeader ("X-Foo" , "2" ).build ());
73
+ final var response = client .performRequest (request );
74
+ var responseMap = responseAsParser (response ).map (HashMap ::new , XContentParser ::list );
75
+ assertThat (responseMap , hasEntry (equalTo ("X-Foo" ), containsInAnyOrder ("1" , "2" )));
76
+ }
77
+
78
+ public void testHeadersTreatedCaseInsensitive () throws IOException {
79
+ final var client = getRestClient ();
80
+ final var request = new Request ("GET" , TestEchoHeadersPlugin .ROUTE );
81
+ request .setOptions (RequestOptions .DEFAULT .toBuilder ().addHeader ("X-Foo" , "1" ).addHeader ("x-foo" , "2" ).build ());
82
+ final var response = client .performRequest (request );
83
+ var responseMap = responseAsParser (response ).map (HashMap ::new , XContentParser ::list );
84
+ assertThat (responseMap , hasEntry (equalTo ("x-foo" ), containsInAnyOrder ("1" , "2" )));
85
+ assertThat (responseMap , hasEntry (equalTo ("X-Foo" ), containsInAnyOrder ("1" , "2" )));
86
+ }
87
+
88
+ public void testThreadContextPopulationFromMultipleHeadersFailsWithCorrectError () {
89
+ final var client = getRestClient ();
90
+ final var sameCaseRequest = new Request ("GET" , TestEchoHeadersPlugin .ROUTE );
91
+ sameCaseRequest .setOptions (
92
+ RequestOptions .DEFAULT .toBuilder ()
93
+ .addHeader ("x-elastic-product-origin" , "elastic" )
94
+ .addHeader ("x-elastic-product-origin" , "other" )
95
+ );
96
+ var exception1 = expectThrows (ResponseException .class , () -> client .performRequest (sameCaseRequest ));
97
+ assertThat (exception1 .getMessage (), containsString ("multiple values for single-valued header [X-elastic-product-origin]" ));
98
+ }
99
+
100
+ public void testMultipleProductOriginHeadersWithDifferentCaseFailsWithCorrectError () {
101
+ final var client = getRestClient ();
102
+ final var differentCaseRequest = new Request ("GET" , TestEchoHeadersPlugin .ROUTE );
103
+ differentCaseRequest .setOptions (
104
+ RequestOptions .DEFAULT .toBuilder ()
105
+ .addHeader ("X-elastic-product-origin" , "elastic" )
106
+ .addHeader ("x-elastic-product-origin" , "other" )
107
+ );
108
+ var exception2 = expectThrows (ResponseException .class , () -> client .performRequest (differentCaseRequest ));
109
+ assertThat (exception2 .getMessage (), containsString ("multiple values for single-valued header [X-elastic-product-origin]" ));
110
+ }
111
+
61
112
public void testMetricsEmittedOnSuccess () throws Exception {
62
113
final var client = getRestClient ();
63
114
final var request = new Request ("GET" , TestEchoStatusCodePlugin .ROUTE );
@@ -125,7 +176,12 @@ private void assertMeasurement(Consumer<Measurement> measurementConsumer) throws
125
176
126
177
@ Override
127
178
protected Collection <Class <? extends Plugin >> nodePlugins () {
128
- return List .of (ChunkedResponseWithHeadersPlugin .class , TestEchoStatusCodePlugin .class , TestTelemetryPlugin .class );
179
+ return List .of (
180
+ ChunkedResponseWithHeadersPlugin .class ,
181
+ TestEchoStatusCodePlugin .class ,
182
+ TestEchoHeadersPlugin .class ,
183
+ TestTelemetryPlugin .class
184
+ );
129
185
}
130
186
131
187
public static class TestEchoStatusCodePlugin extends Plugin implements ActionPlugin {
@@ -181,6 +237,62 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli
181
237
}
182
238
}
183
239
240
+ public static class TestEchoHeadersPlugin extends Plugin implements ActionPlugin {
241
+ static final String ROUTE = "/_test/echo_headers" ;
242
+ static final String NAME = "test_echo_headers" ;
243
+
244
+ private static final Logger logger = LogManager .getLogger (TestEchoStatusCodePlugin .class );
245
+
246
+ @ Override
247
+ public Collection <RestHandler > getRestHandlers (
248
+ Settings settings ,
249
+ NamedWriteableRegistry namedWriteableRegistry ,
250
+ RestController restController ,
251
+ ClusterSettings clusterSettings ,
252
+ IndexScopedSettings indexScopedSettings ,
253
+ SettingsFilter settingsFilter ,
254
+ IndexNameExpressionResolver indexNameExpressionResolver ,
255
+ Supplier <DiscoveryNodes > nodesInCluster ,
256
+ Predicate <NodeFeature > clusterSupportsFeature
257
+ ) {
258
+ return List .of (new BaseRestHandler () {
259
+ @ Override
260
+ public String getName () {
261
+ return NAME ;
262
+ }
263
+
264
+ @ Override
265
+ public List <Route > routes () {
266
+ return List .of (new Route (RestRequest .Method .GET , ROUTE ), new Route (RestRequest .Method .POST , ROUTE ));
267
+ }
268
+
269
+ @ Override
270
+ protected RestChannelConsumer prepareRequest (RestRequest request , NodeClient client ) {
271
+ var headers = request .getHeaders ();
272
+ logger .info ("received header echo request for [{}]" , String .join ("," , headers .keySet ()));
273
+
274
+ return channel -> {
275
+ final var response = RestResponse .chunked (
276
+ RestStatus .OK ,
277
+ ChunkedRestResponseBodyPart .fromXContent (
278
+ params -> Iterators .concat (
279
+ ChunkedToXContentHelper .startObject (),
280
+ Iterators .map (headers .entrySet ().iterator (), e -> (b , p ) -> b .field (e .getKey (), e .getValue ())),
281
+ ChunkedToXContentHelper .endObject ()
282
+ ),
283
+ request ,
284
+ channel
285
+ ),
286
+ null
287
+ );
288
+ channel .sendResponse (response );
289
+ logger .info ("sent response" );
290
+ };
291
+ }
292
+ });
293
+ }
294
+ }
295
+
184
296
public static class ChunkedResponseWithHeadersPlugin extends Plugin implements ActionPlugin {
185
297
186
298
static final String ROUTE = "/_test/chunked_response_with_headers" ;
0 commit comments