Add RequestParams to handle repeated query parameters#144506
Add RequestParams to handle repeated query parameters#144506felixbarny merged 36 commits intoelastic:mainfrom
RequestParams to handle repeated query parameters#144506Conversation
The existing decodeQueryString fills a Map<String,String> so repeated parameters (e.g. match[]=foo&match[]=bar) silently drop all but the last value. Add decodeQueryStringMulti that returns Map<String,List<String>> and preserves every occurrence. Internally the parsing loop is extracted into a private parseQueryStringPairs helper that accepts a BiConsumer<String,String>, allowing both the single-value and multi-value variants to share the same decoding logic without duplication.
|
Pinging @elastic/es-storage-engine (Team:StorageEngine) |
kkrik-es
left a comment
There was a problem hiding this comment.
Let's also get a review from Distributed.
|
Pinging @elastic/es-distributed (Team:Distributed) |
|
Pinging @elastic/es-core-infra (Team:Core/Infra) |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughQuery string parameter handling has been refactored to support multi-valued parameters (repeated parameter names). A new ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Adds ParameterMap, a Map<String,String> that preserves multiple values per key via getAll()/getSingle(), and migrates all call sites that created a local HashMap just to pass to decodeQueryString over to the new decodeQueryStringMulti() which returns a ParameterMap directly. Also removes the now-dead decodeQueryString(URI, Map) overload.
Replaces the decodeQueryString + local HashMap pattern in both HttpRequest.Builder and HttpRequestTemplate.Builder fromUrl() with decodeQueryStringMulti, keeping all Map<String, String> signatures unchanged.
…from factory methods
Mutation methods (put, remove, clear) now throw UnsupportedOperationException. Value lists are defensively copied via List.copyOf in the constructor, and keySet/entrySet no longer expose mutable views. Tests updated accordingly.
DaveCTurner
left a comment
There was a problem hiding this comment.
Neat. A few interface nits but overall this seems like a good approach.
…decodeQueryString overload
…tating immutable RequestParams Add markerParams Set as an overlay for internal marker parameters (serverlessRequest, operatorRequest) so that setParamTrueOnceAndConsume no longer calls params.put() which throws UnsupportedOperationException on the immutable RequestParams. The overlay is propagated via the copy constructor and is visible through hasParam(), param(), and paramAsBoolean() (the ToXContent.Params interface).
… overlay Instead of keeping RequestParams immutable and adding overlay maps to work around callsites that legitimately mutate params (e.g. RestIndexAction, RestClusterRerouteAction, SettingsFilter, test code), restore the pre-PR mutable put() semantics on RequestParams directly. The value lists stored per key remain immutable (List.of()), so a non-null result from getAll() always guarantees getFirst()/getLast() are safe to call. The shared empty() singleton is replaced with a fresh instance per call to avoid shared-state mutation bugs. This also reverts the markerParams overlay commit: setParamTrueOnceAndConsume goes back to calling params.put() directly, and the markerParams field, copy-constructor propagation, and hasParam()/param() checks are all removed.
Catch IllegalArgumentException in RequestParams.fromQueryString and rethrow as RestRequest.BadParameterException, matching the existing pattern in requireSingle(). This makes the private static RestRequest.params(String) wrapper redundant, so remove it and call RequestParams.fromUri() directly from RestRequest.request().
…ception wrapping fromUri called decodeQueryString directly, bypassing the try/catch added in fromQueryString. Delegate to fromQueryString instead so all three parsing entry points (fromQueryString, fromUri, from(URI)) consistently wrap IllegalArgumentException as BadParameterException.
…Params AbstractMap's default implementations of these methods iterate the entry set, which failed with UnsupportedOperationException before this change. Now all three delegate directly to the backing LinkedHashMap, fixing the crash observed via RestController.clear() in integration tests.
fromSingleValues now maps null values to "" to match the previous Map<String,String> behaviour where null meant a valueless parameter. RestUtilsTests.testReservedParameters updated to expect BadParameterException (wrapping IllegalArgumentException) since fromUri now wraps all parse errors in BadParameterException.
- decodeQueryString now returns RequestParams directly, built via the new package-private addValue() method, eliminating the intermediate Map<String, List<String>> - addParam takes RequestParams instead of Map<String, List<String>> - fromQueryString no longer needs the of() wrapping step - Backing field widened to Map<String, List<String>>; copy logic moved into of() - getAll() returns Collections.unmodifiableList to prevent callers mutating the backing list - put() uses Collections.singletonList (no ArrayList needed for replacements)
RequestParams to handle repeated query parameters
DaveCTurner
left a comment
There was a problem hiding this comment.
Neat 😁 just a few nits about test coverage but otherwise this looks good.
server/src/test/java/org/elasticsearch/rest/RequestParamsTests.java
Outdated
Show resolved
Hide resolved
test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java
Show resolved
Hide resolved
- Cover hasNext() in entrySet iterator test - Add testEntrySetClear() for entrySet().clear() - Add testAddValue() for the package-private addValue() method - Add testFromUri() and testFrom() for the URI factory methods - Add testDecodeQueryStringMultipleValuesUnadorned() for bare repeated params
The existing decodeQueryString fills a Map<String,String> so repeated parameters (e.g. match[]=foo&match[]=bar) silently drop all but the last value. Adds a RequestParams that supports multiple values for a parameter.
The existing decodeQueryString fills a Map<String,String> so repeated parameters (e.g. match[]=foo&match[]=bar) silently drop all but the last value. Adds a RequestParams that supports multiple values for a parameter.
Introduces
RequestParams, aMap<String, String>view over HTTP request parameters that natively preserves multiple values per key (e.g.match[]=foo&match[]=bar).Motivation
The existing
decodeQueryStringfills aMap<String, String>so repeated parameters silently drop all but the last value. This is required by #144494 which implements the Prometheus series API endpoint that relies on multiple occurrences ofmatch[].What changed
RequestParams(new class, replaces ad-hocMap<String, String>usage):AbstractMap<String, String>— the standardMapinterface operates on the last value for each key, preserving backwards compatibility with all existing single-value callsitesgetAll(String)returns all values for a repeated parameter as a non-empty, immutableList<String>(sogetFirst()/getLast()are always safe on a non-null result)requireSingle(String)asserts that a key has exactly one value, throwingBadParameterExceptionotherwiseput(String, String)is mutable and stores the value as a one-element immutable list, preserving the non-empty list invariantfromQueryString,fromUri(String), andfrom(URI)replace the previousRestUtils.decodeQueryStringoverloads and wrap parse errors asRestRequest.BadParameterExceptionRestRequest:params()returnsRequestParamsinstead ofMap<String, String>repeatedParamAsListdelegates toRequestParams.getAll()Stack:
RequestParamsto handle repeated query parameters #144506 ← (this PR)