Skip to content

Query Params as Lists in SpEL, Groovy#2789

Merged
predic8 merged 4 commits into
masterfrom
query-param-as-list
Feb 16, 2026
Merged

Query Params as Lists in SpEL, Groovy#2789
predic8 merged 4 commits into
masterfrom
query-param-as-list

Conversation

@predic8

@predic8 predic8 commented Feb 13, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • New Features

    • Query parameters now return as arrays to support multiple values with the same parameter name
  • Documentation

    • Updated configuration examples and templates to demonstrate accessing multi-valued parameters using array indexing and optional chaining syntax (e.g., params.city?.[0])
  • Tests

    • Added tests validating multi-valued parameter handling and array indexing in parameter access

…rategy, update logic to support lists, and adjust affectd components and tests
…tegy` import, remove unused `ParamBuilder`, and clean up `TemplateInterceptorTest`.
@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This pull request extends Membrane's query parameter handling from single-value assumptions to multi-valued support across the codebase. URLParamUtil is refactored to return Map<String, List<String>>, while expression evaluators (SpEL, Groovy) and template engines are updated to access parameters via indexing (e.g., params.a[0]). Documentation examples and tests are revised accordingly. Multiple files also receive Apache 2.0 license headers.

Changes

Cohort / File(s) Summary
Core Parameter Utility Refactoring
core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java
Major refactoring introducing multi-valued parameter support via Map<String, List<String>> return type. Adds overloaded parseQueryString methods, centralizes URI parsing logic, removes deprecated ParamBuilder class, and maintains backward-compatible single-value variants with DuplicateKeyOrInvalidFormStrategy.
Expression Engine Updates
core/src/main/java/com/predic8/membrane/core/lang/spel/SpELExchangeExpression.java, core/src/main/java/com/predic8/membrane/core/lang/spel/SpELExchangeEvaluationContext.java, core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java
Updated expression evaluation to handle list-based parameters and optional chaining. SpELExchangeExpression now converts List values to first-element strings when string conversion requested. SpELExchangeEvaluationContext and ScriptingUtils simplified to use default parameter parsing without explicit merge strategy.
HTTP URI Handling
core/src/main/java/com/predic8/membrane/core/http/xml/URI.java
Updated imports and replaced explicit URLParamUtil.DuplicateKeyOrInvalidFormStrategy.MERGE_USING_COMMA references with static imports. Removed UnsupportedEncodingException declaration from method signature.
Template & Example Files
distribution/examples/templating/json/apis.yaml, distribution/examples/templating/text/apis.yaml, distribution/examples/extending-membrane/error-handling/custom-error-messages/apis.yaml, distribution/examples/extending-membrane/if/apis.yaml, distribution/examples/scripting/javascript/proxies.xml, distribution/examples/scripting/javascript/apis.yaml
Examples updated to access first parameter element via indexing syntax (e.g., params.city?.[0], params.answer?[0]). Removed commented target blocks, replaced with explicit return status codes. JavaScript examples now use .get(0) or ?.[0] for element access.
Test Updates - Expression & Scripting
core/src/test/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpressionTest.java, core/src/test/java/com/predic8/membrane/core/lang/spel/SpELExchangeExpressionTest.java, core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxyKeyComplexMatchTest.java
New tests added validating multi-valued parameter handling and optional chaining support. GroovyExchangeExpressionTest includes multipleIdenticalQueryParams test. SpELExchangeExpressionTest adds paramsIndex test. APIProxyKeyComplexMatchTest refactored to use param[foo]?.[0] expression syntax.
Test Updates - Templates & Utilities
core/src/test/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptorTest.java, core/src/test/java/com/predic8/membrane/core/util/URLParamUtilTest.java, distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/TextTemplateExampleTest.java, distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/JavascriptExampleTest.java
Template tests updated to expect array indexing (e.g., [1] instead of 1). URLParamUtilTest expanded with parseQueryString validation tests. Example tests refactored with setup methods and assertion updates. JavascriptExampleTest redesigned with getExampleDirName() override and test lifecycle management.
License Header Additions
core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/AbstractUserDataProvider.java, core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/User.java, core/src/main/java/com/predic8/membrane/core/util/security/BasicAuthenticationUtil.java, core/src/test/java/com/predic8/membrane/core/http/PatchWithoutBodyTest.java, core/src/test/java/com/predic8/membrane/core/util/security/BasicAuthenticationUtilTest.java
Apache License 2.0 header comments added to multiple files with no functional code changes.
README Documentation
README.md
Query parameter examples updated to use optional chaining and array indexing syntax (e.g., params.city?.[0] instead of params.city).
Test Infrastructure
core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java
TestBeanRegistry class augmented with Object getBean(String beanName) method override to support bean resolution by name in tests.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Request Handler
    participant URLParamUtil
    participant Expression Evaluator
    participant Template Engine
    participant Response

    Client->>Request Handler: HTTP request (query: a=1&a=2)
    Request Handler->>URLParamUtil: parseQueryString(queryStr)
    URLParamUtil->>URLParamUtil: Extract key-value pairs
    URLParamUtil-->>Request Handler: Map<String, List<String>><br/>{a: ["1", "2"]}
    
    Request Handler->>Expression Evaluator: evaluate("params.a?.[0]")<br/>with params map
    Expression Evaluator->>Expression Evaluator: Extract first element<br/>from list via indexing
    Expression Evaluator-->>Request Handler: "1" (String result)
    
    Request Handler->>Template Engine: render(template,<br/>params with lists)
    Template Engine->>Template Engine: Access params.a[0]<br/>via template syntax
    Template Engine-->>Request Handler: Rendered output<br/>with first values
    
    Request Handler-->>Response: HTTP response
    Response-->>Client: Response with data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

7.x

Suggested reviewers

  • rrayst

Poem

🐰 Hops with glee through param arrays,
Where values once singular, now plural plays!
Optional chaining guards the way,
First elements shine in brackets' display,
Multi-valued queries dance and sway!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: refactoring query parameters to be represented as lists in SpEL and Groovy expression contexts, which aligns with the substantial modifications to URLParamUtil, parameter binding logic, and related test updates.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch query-param-as-list

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java (1)

371-374: Consider delegating to refs map for consistency with resolve().

resolve(String) on line 333 performs an actual lookup in the refs map, but getBean(String) unconditionally returns null. If any future test or framework code calls getBean instead of resolve, registered beans won't be found. This is fine as a minimal stub to satisfy the interface, but worth noting.

Optional: align with `resolve`
         `@Override`
         public Object getBean(String beanName) {
-            return null;
+            return refs.get(beanName);
         }
core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java (1)

159-174: Multi-value parseQueryString silently drops non-matching segments.

Unlike the single-value overload which can throw on malformed segments (with ERROR strategy), the multi-value parseQueryString(String) silently skips segments that don't match paramsPat. Given that paramsPat = ([^=]*)=?(.*) matches virtually anything (including bare keys), this is unlikely to be an issue in practice. However, for parity, consider whether a log warning on unmatched segments would be helpful for debugging.

core/src/test/java/com/predic8/membrane/core/util/URLParamUtilTest.java (1)

88-100: Consider adding happy-path tests for the multi-value parseQueryString.

The new tests cover null/empty edge cases well, but there are no direct unit tests for the multi-value overload's core behavior (e.g., "a=1&a=2"{a: ["1", "2"]}, or a mix of single and multi-value keys). The integration tests in GroovyExchangeExpressionTest cover this indirectly, but a direct unit test here would provide faster feedback and better isolation.

💡 Suggested additional tests
+    `@Test`
+    void testParseQueryStringMultiValue() {
+        var result = parseQueryString("a=1&a=2&b=3");
+        assertEquals(List.of("1", "2"), result.get("a"));
+        assertEquals(List.of("3"), result.get("b"));
+    }
+
+    `@Test`
+    void testParseQueryStringSingleValue() {
+        var result = parseQueryString("x=hello");
+        assertEquals(List.of("hello"), result.get("x"));
+    }

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
distribution/examples/templating/text/apis.yaml (1)

42-44: ⚠️ Potential issue | 🟡 Minor

Param values are now lists — iteration output will change.

Since params are now Map<String, List<String>>, p.value in this template loop will render as [value] (list toString) rather than the bare value. If this example is meant to demonstrate readable param output, you may want to handle the list (e.g., join or iterate the values).

core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java (1)

67-74: ⚠️ Potential issue | 🟠 Major

New getParams(URIFactory, Exchange) is missing form-encoded body parameter fallback — breaking change for Groovy scripts and templates.

The multi-value getParams() method at URLParamUtil lines 37-44 only extracts parameters from the URI query string (line 43). It lacks the form body fallback present in the old getParams(URIFactory, Exchange, Strategy) method (lines 54-58), which checks for application/x-www-form-urlencoded body when the query string is absent.

Groovy scripts or templates that access form-encoded POST parameters via params or param bindings will now receive an empty map instead of the form data. ScriptingUtils line 69 uses this new variant.

🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/lang/spel/SpELExchangeExpression.java`:
- Around line 82-85: The code in SpELExchangeExpression handling List-to-String
conversion must guard against empty lists and null first elements; in the block
that checks "if (o instanceof List<?> l)" add checks like "if (l.isEmpty())
return null;" and "var first = l.getFirst(); if (first == null) return null;"
then convert with "type.cast(first.toString())" (or use Objects.toString(first,
null)) so you don't throw NoSuchElementException or NPE when a List is empty or
its first element is null.

In `@core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java`:
- Around line 138-153: parseQueryString currently treats an empty string query
"" as a segment and, because paramsPat matches it, adds a {"": [""]} entry; fix
this by short-circuiting empty queries (similar to the existing null check) or
by skipping empty segments before applying paramsPat: update parseQueryString to
return the empty params map when query.isEmpty() or to continue the loop when
p.isEmpty() so paramsPat.matcher(p) is not invoked and no empty key/value is
inserted.
- Around line 37-44: The multi-value getParams(URIFactory, Exchange) in
URLParamUtil currently returns parseQueryString(getUri(...).getRawQuery()) and
omits the form-encoded body fallback present in the single-value overload;
update this multi-value overload to mirror the single-value logic by checking if
getRawQuery() is null/empty and, if so and the request Content-Type is
"application/x-www-form-urlencoded", read the request body (via the Exchange
request body accessor used by the single-value method), call parseQueryString on
that body and return the resulting Map<String,List<String>>; ensure you use the
same helpers (getUri, parseQueryString) and preserve existing exception
handling/return of emptyMap() when appropriate.

In `@distribution/examples/scripting/javascript/apis-preview.yaml`:
- Line 22: The example currently calls params.get('id').get(0) and
params.get('city').get(0) with no null guard; update the example to either add a
null check/optional chaining around params.get('id') and params.get('city')
before calling .get(0) (e.g., guard or use ?.get(0)) or add a short comment
above the expression stating that id and city are required query parameters so
readers know the happy-path assumption; target the expression using the symbols
params.get('id'), params.get('city') and the .get(0) calls to locate and fix the
code.

In `@distribution/examples/templating/text/apis.yaml`:
- Line 11: The template uses a non-null-safe access params.name.get(0) which
will throw if the name query param is missing; replace it with the null-safe
pattern used elsewhere (e.g., params?.name?.[0] or params.name?[0]) so the
template reads something like "src: Hello ${params?.name?.[0]}!" to avoid an NPE
when the param is absent.

In `@README.md`:
- Line 350: Update the SpEL optional chaining used in the JSON snippet by
replacing the incorrect `${params.answer?[0]}` token with the correct
`${params.answer?.[0]}` form so safe index access uses the dot-before-bracket
syntax; locate the occurrence of `${params.answer?[0]}` in the README and change
it to `${params.answer?.[0]}` to match the working example `params.city?.[0]`.
🧹 Nitpick comments (4)
core/src/test/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpressionTest.java (2)

104-115: Consider using assertInstanceOf for clearer failure messages.

The if/else fail() pattern loses type information in failure output. Also note the existing tests (line 72) use the inverse check with early return — this test uses a different pattern for the same purpose.

Minor: a more idiomatic JUnit 5 approach:

♻️ Suggested refactor
-        var o = expression(new InterceptorAdapter(router), getLanguage(), "params.a").evaluate(exc, REQUEST, Object.class);
-        if (o instanceof List l) {
-            assertEquals(2, l.size());
-            assertEquals("1", l.get(0));
-            assertEquals("2", l.get(1));
-        } else fail();
+        var o = expression(new InterceptorAdapter(router), getLanguage(), "params.a").evaluate(exc, REQUEST, Object.class);
+        assertInstanceOf(List.class, o);
+        List l = (List) o;
+        assertEquals(2, l.size());
+        assertEquals("1", l.get(0));
+        assertEquals("2", l.get(1));

117-119: Minor inconsistency: hardcoded GROOVY vs getLanguage().

Line 107 uses getLanguage() while this helper hardcodes GROOVY. They're equivalent here, but using getLanguage() would be consistent. Also, extra whitespace before ) on line 117.

♻️ Suggested fix
-    private String evaluateQueryParam(Exchange exc, String expr  ) {
-        return expression(new InterceptorAdapter(router), GROOVY, expr).evaluate(exc, REQUEST, String.class);
+    private String evaluateQueryParam(Exchange exc, String expr) {
+        return expression(new InterceptorAdapter(router), getLanguage(), expr).evaluate(exc, REQUEST, String.class);
     }
distribution/examples/templating/json/apis.yaml (1)

13-13: Potential null risk if answer param is missing.

Other examples in this PR use double optional chaining (e.g., params?.answer?.[0]), but here params.answer?[0] only null-guards the index access. If the answer key is absent and params.answer returns null, this should still be safe due to ?[0]. However, the inconsistency with other examples (which also guard the property access) is worth noting — consider aligning to params.answer?.[0] or params?.answer?.[0] for consistency.

README.md (1)

679-679: Clarify syntax inconsistency and edge case behavior.

This line uses params.answer?[0] (without a dot), while line 350 uses params.city?.[0] (with a dot). If both are valid syntax for their respective contexts, this should be documented to avoid user confusion. If one is incorrect, it should be corrected.

Additionally, consider documenting the behavior when:

  • The answer parameter is not provided in the query string
  • The parameter is provided but has an empty value (e.g., ?answer=)
  • Multiple values are provided (e.g., ?answer=42&answer=43)
📝 Consider adding a comment or note in the example
+ # Query parameters are arrays - use ?[0] to safely access the first value
  api:
    port: 2000
    flow:
      - request:
          - template:
              contentType: application/json
              pretty: true
              src: |
                { "answer": ${params.answer?[0]} }

Comment thread core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java
Comment thread distribution/examples/scripting/javascript/apis-preview.yaml Outdated
Comment thread distribution/examples/templating/text/apis.yaml Outdated
Comment thread README.md
…move unused tests and imports, add null checks, and improve YAML and JSON parsing.
@predic8 predic8 merged commit e46dae6 into master Feb 16, 2026
5 checks passed
@predic8 predic8 deleted the query-param-as-list branch February 16, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants