Skip to content

Commit 8d185e6

Browse files
committed
Preserve multi-value request params through PathTrie iteration
RestController#getAllHandlers was using Map.copyOf(requestParamsRef) to snapshot the original params before PathTrie iterations. Map.copyOf only sees the last value per key (via entrySet), silently dropping earlier values for repeated parameters like ?format=json&format=yaml. - Add RequestParams.copyOf(RequestParams) to deep-copy all multi-values - Add RequestParams.putAll(RequestParams) overload that copies every value in each key's list, unlike the inherited putAll(Map) which only sees the last value per key via entrySet - Change getAllHandlers to accept RequestParams instead of Map<String,String> and use the new methods - Add tests for copyOf, putAll(RequestParams), and a regression test in RestControllerTests
1 parent 614b172 commit 8d185e6

4 files changed

Lines changed: 75 additions & 2 deletions

File tree

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,31 @@ public static RequestParams fromSingleValues(Map<String, String> singleValues) {
122122
return new RequestParams(wrapped);
123123
}
124124

125+
/**
126+
* Returns an independent copy of {@code source}, preserving all multi-values.
127+
*
128+
* @param source the {@code RequestParams} to copy
129+
* @return a new mutable {@code RequestParams} with the same contents as {@code source}
130+
*/
131+
public static RequestParams copyOf(RequestParams source) {
132+
LinkedHashMap<String, List<String>> copy = Maps.newLinkedHashMapWithExpectedSize(source.map.size());
133+
source.map.forEach((k, v) -> copy.put(k, List.copyOf(v)));
134+
return new RequestParams(copy);
135+
}
136+
125137
private RequestParams(Map<String, List<String>> map) {
126138
this.map = map;
127139
}
128140

141+
/**
142+
* Copies all entries from {@code source} into this map, preserving all multi-values.
143+
* Unlike the inherited {@link #putAll(Map)}, which only sees the last value per key,
144+
* this overload copies every value in each key's list.
145+
*/
146+
public void putAll(RequestParams source) {
147+
source.map.forEach(map::put);
148+
}
149+
129150
/**
130151
* Appends {@code value} to the list of values for {@code key}.
131152
* If the key is not yet present, a new entry is created.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,14 +650,14 @@ private static void validateErrorTrace(RestRequest request, RestChannel channel)
650650
}
651651
}
652652

653-
Iterator<MethodHandlers> getAllHandlers(@Nullable Map<String, String> requestParamsRef, String rawPath) {
653+
Iterator<MethodHandlers> getAllHandlers(@Nullable RequestParams requestParamsRef, String rawPath) {
654654
final Supplier<Map<String, String>> paramsSupplier;
655655
if (requestParamsRef == null) {
656656
paramsSupplier = () -> null;
657657
} else {
658658
// Between retrieving the correct path, we need to reset the parameters,
659659
// otherwise parameters are parsed out of the URI that aren't actually handled.
660-
final Map<String, String> originalParams = Map.copyOf(requestParamsRef);
660+
final RequestParams originalParams = RequestParams.copyOf(requestParamsRef);
661661
paramsSupplier = () -> {
662662
// PathTrie modifies the request, so reset the params between each iteration
663663
requestParamsRef.clear();

server/src/test/java/org/elasticsearch/rest/RequestParamsTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,43 @@ public void testFrom() throws Exception {
178178
assertThat(map.get("b"), equalTo("2"));
179179
assertThat(RequestParams.from(new URI("http://example.com/path")).isEmpty(), is(true));
180180
}
181+
182+
public void testCopyOfPreservesMultiValues() {
183+
var original = RequestParams.of(Map.of("format", List.of("json", "yaml"), "size", List.of("10")));
184+
var copy = RequestParams.copyOf(original);
185+
assertThat(copy.getAll("format"), equalTo(List.of("json", "yaml")));
186+
assertThat(copy.get("format"), equalTo("yaml"));
187+
assertThat(copy.getAll("size"), equalTo(List.of("10")));
188+
assertThat(copy.size(), equalTo(2));
189+
}
190+
191+
public void testCopyOfIsIndependentOfSource() {
192+
var original = RequestParams.of(Map.of("format", List.of("json", "yaml")));
193+
var copy = RequestParams.copyOf(original);
194+
original.clear();
195+
assertThat(copy.getAll("format"), equalTo(List.of("json", "yaml")));
196+
assertThat(original.isEmpty(), is(true));
197+
}
198+
199+
public void testCopyOfSourceDoesNotReflectMutationsToTheCopy() {
200+
var original = RequestParams.of(Map.of("format", List.of("json", "yaml")));
201+
var copy = RequestParams.copyOf(original);
202+
copy.put("format", "cbor");
203+
assertThat(original.getAll("format"), equalTo(List.of("json", "yaml")));
204+
}
205+
206+
public void testPutAllRequestParamsPreservesMultiValues() {
207+
var source = RequestParams.of(Map.of("format", List.of("json", "yaml")));
208+
var target = RequestParams.empty();
209+
target.putAll(source);
210+
assertThat(target.getAll("format"), equalTo(List.of("json", "yaml")));
211+
}
212+
213+
public void testPutAllRequestParamsMergesIntoExistingEntries() {
214+
var source = RequestParams.of(Map.of("format", List.of("json", "yaml")));
215+
var target = RequestParams.of(Map.of("size", List.of("10")));
216+
target.putAll(source);
217+
assertThat(target.getAll("format"), equalTo(List.of("json", "yaml")));
218+
assertThat(target.getAll("size"), equalTo(List.of("10")));
219+
}
181220
}

server/src/test/java/org/elasticsearch/rest/RestControllerTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,19 @@ public void testApiProtectionWithServerlessEnabledAsEndUser() {
11881188
checkUnprotected.accept(inaccessiblePaths);
11891189
}
11901190

1191+
public void testGetAllHandlersPreservesMultiValueParams() {
1192+
restController.registerHandler(new Route(GET, "/{index}"), (request, channel, client) -> {});
1193+
1194+
var params = RequestParams.of(Map.of("format", List.of("json", "yaml")));
1195+
var it = restController.getAllHandlers(params, "/my-index");
1196+
while (it.hasNext()) {
1197+
it.next();
1198+
}
1199+
1200+
// Multi-values must survive the per-iteration reset that PathTrie triggers
1201+
assertThat(params.getAll("format"), equalTo(List.of("json", "yaml")));
1202+
}
1203+
11911204
@ServerlessScope(Scope.PUBLIC)
11921205
private static final class PublicRestHandler extends BaseRestHandler {
11931206
@Override

0 commit comments

Comments
 (0)