Skip to content

fix: compute URL expressions in the target after request flow#2790

Merged
rrayst merged 19 commits into
masterfrom
compute-url-expression-at-http-client-interceptor
Feb 20, 2026
Merged

fix: compute URL expressions in the target after request flow#2790
rrayst merged 19 commits into
masterfrom
compute-url-expression-at-http-client-interceptor

Conversation

@predic8
Copy link
Copy Markdown
Member

@predic8 predic8 commented Feb 15, 2026

…http client interceptor

Summary by CodeRabbit

  • New Features

    • Added URL encoding and path segment encoding functions for templating languages (Groovy, SpEL, JSONPath, XPath).
    • Added ability to change HTTP request methods dynamically and clear request bodies.
    • Support for expression-based URL template evaluation with configurable escaping strategies.
  • Bug Fixes

    • Improved error handling for malformed and invalid URIs.
    • Enhanced validation of URI components with better security safeguards.
  • Improvements

    • URL template expressions now evaluated at the appropriate stage in request processing.
    • Better control over character escaping in dynamic URL construction.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 15, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors URI handling and expression evaluation in the framework. It introduces runtime URL template evaluation with customizable escaping strategies, replaces compile-time Target URL computation with runtime applyModifications(), adds comprehensive URI validation utilities, and restructures HTTP method/body modification flows through new target modification mechanisms.

Changes

Cohort / File(s) Summary
HTTP Message & Request Handling
core/src/main/java/com/predic8/membrane/core/http/Message.java, core/src/main/java/com/predic8/membrane/core/http/Request.java
Added emptyBody() method to clear body content and remove Content-Length/Type/Encoding headers; added changeMethod() to update HTTP method and conditionally clear body for methods without payloads.
Interceptor Request Processing
core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java, core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java
Refactored error handling to detect and report invalid URI characters via ProblemDetails; delegated target modifications to new runtime applyTargetModifications() pathway instead of inline method changes; added URI factory support for lenient character parsing.
Built-in Encoding Functions
core/src/main/java/com/predic8/membrane/core/lang/CommonBuiltInFunctions.java, core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyBuiltInFunctions.java, core/src/main/java/com/predic8/membrane/core/lang/spel/functions/SpELBuiltInFunctions.java
Added urlEncode() and pathEncode() utility methods to CommonBuiltInFunctions; exposed wrappers in Groovy and SpEL variants for use in template expressions.
Target & Proxy URL Resolution
core/src/main/java/com/predic8/membrane/core/proxies/Target.java, core/src/main/java/com/predic8/membrane/core/util/URLUtil.java
Replaced compile-time compileUrl() with runtime applyModifications(); introduced expression evaluation gating and escaping strategies; changed getHost() to getAuthority(); added getPortFromURL().
Core URI Parsing & Validation
core/src/main/java/com/predic8/membrane/core/util/URI.java, core/src/main/java/com/predic8/membrane/core/util/URIFactory.java, core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java, core/src/main/java/com/predic8/membrane/core/util/UriIllegalCharacterDetector.java, core/src/main/java/com/predic8/membrane/core/util/ip/IPv6Util.java
Replaced java.net.URI delegation with custom parsing supporting illegal-character tolerance; added RFC 3986-compliant validation; introduced resolve() methods for URI reference resolution; added IPv6 validation.
URI Resolution & File Utilities
core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java, core/src/main/java/com/predic8/membrane/core/util/FileUtil.java, core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java
Reworked path combination to be URIFactory-aware; added file-to-URI conversion and path resolution helpers; introduced containsTemplateMarker() utility.
Template Expression Handling
core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java, core/src/main/java/com/predic8/membrane/core/util/uri/EscapingUtil.java
Added encoder function parameter to template evaluation for escaping non-text tokens; introduced Escaping enum with NONE, URL, and SEGMENT strategies for safe URI component substitution.
Interceptor & URI Routing
core/src/main/java/com/predic8/membrane/core/interceptor/InternalRoutingInterceptor.java, core/src/main/java/com/predic8/membrane/core/proxies/RuleManager.java, core/src/main/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptor.java
Updated destination lookups to use getAuthority() instead of getHost(); extended error handling to catch IllegalArgumentException for invalid URL parsing.
Test Coverage
core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java, core/src/test/java/com/predic8/membrane/core/interceptor/acl/targets/TargetTest.java, core/src/test/java/com/predic8/membrane/core/proxies/.../*, core/src/test/java/com/predic8/membrane/core/util/.../*, core/src/test/java/com/predic8/membrane/core/resolver/.../*, core/src/test/java/com/predic8/membrane/core/openapi/.../*
Expanded test suite with expression evaluation tests across GROOVY, SPEL, JSONPath, XPath languages; added URI resolution and escaping validation tests; updated existing tests for new API surface and runtime evaluation semantics.
Documentation & Configuration
docs/ROADMAP.md, distribution/examples/configuration/apis.yaml, distribution/src/test/java/com/predic8/membrane/tutorials/.../*
Updated roadmap for 7.1.1 improvements; clarified allowIllegalCharacters security implications; adjusted tutorial test assertions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DispatchingInterceptor
    participant Target
    participant URIFactory
    participant TemplateExpressionEvaluator
    
    Client->>DispatchingInterceptor: handleRequest(Exchange)
    DispatchingInterceptor->>Target: applyModifications(Exchange, Router)
    Target->>TemplateExpressionEvaluator: evaluateTemplate(targetURL, Exchange)
    TemplateExpressionEvaluator->>URIFactory: create(evaluatedURL)
    URIFactory-->>TemplateExpressionEvaluator: URI with safe characters
    TemplateExpressionEvaluator-->>Target: resolved destination
    Target->>Target: applyEscaping(result)
    Target->>Target: applyMethodOverride(Exchange)
    Target-->>DispatchingInterceptor: modified Exchange
    DispatchingInterceptor-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #2123: Modifies DispatchingInterceptor error handling to produce ProblemDetails for invalid/illegal request URIs with enhanced detection pathways.
  • PR #2322: Adds and extends encoding utility functions (urlEncode(), pathEncode()) across CommonBuiltInFunctions, Groovy, and SpEL variants.
  • PR #2253: Refactors TemplateExchangeExpression construction API by modifying factory signatures and token parsing logic to support encoder parameters.

Suggested labels

7.x

Suggested reviewers

  • rrayst

Poem

A URI adventure, spring to life—
With placeholders resolved, no strife!
Templates evaluate with escaping grace,
Path segments encoded in cyberspace. 🐰✨
Braces and slashes, now handled with care,
Runtime expressions floating in air!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: compute URL expressions in the target after request flow' accurately describes the main architectural change: moving URL template expression evaluation to occur after the request flow rather than before, specifically in the Target class and HTTP client interceptor.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch compute-url-expression-at-http-client-interceptor

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java (1)

110-122: ⚠️ Potential issue | 🟡 Minor

Use lenient URI parsing consistently or skip resolution for expression URLs with root basePath.

Lines 26 and 28 use incompatible URI parsers: UriUtil.getPathFromURL(new URIFactory(true), targetURL) on line 26 tolerates expression syntax (${}), but the standard java.net.URI(targetURL) constructor on line 28 does not. When a target URL contains expressions in the authority (e.g., http://${host}/) and evaluates to a root basePath, line 28 throws URISyntaxException despite the intent (expressed in the comment) to support such expressions.

The proposed check for "${" is a pragmatic defensive fix:

if (basePath == null || basePath.isEmpty() || "/".equals(basePath)) {
+   if (targetURL.contains("${")) {
+       return targetURL;
+   }
    URI base = new URI(targetURL);
    // Resolve and normalize slashes consistently with the branch below.
    return base.resolve(getUri(exc)).toString();
}
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/predic8/membrane/core/http/Message.java`:
- Around line 89-101: Replace the empty warn log and add removal of
TRANSFER_ENCODING in Message.emptyBody: instead of log.warn("", e) use the same
logging style as discardBody (log.debug with a short descriptive message and the
exception) to handle the IOException, and after setting body = new EmptyBody()
remove the TRANSFER_ENCODING header (call
header.removeFields(TRANSFER_ENCODING)) similar to what setBodyContent() does;
update Message.emptyBody to mirror discardBody's logging level/format and to
clear the transfer-encoding header.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java`:
- Line 43: The constant PROXIES_HINT in HTTPClientInterceptor currently
references the wrong config filename; update its string from "conf/apies.xml" to
"conf/proxies.xml" so the hint points to the correct configuration file (edit
the PROXIES_HINT field in class HTTPClientInterceptor to replace "apies.xml"
with "proxies.xml").
- Around line 70-73: The call to applyTargetModifications in
HTTPClientInterceptor.handleRequest is executed before the surrounding try block
so any runtime exceptions from expression evaluation (Target.applyModifications
→ computeDestinationExpressions, e.g., SpelEvaluationException or NPE) escape
the existing handlers; move the applyTargetModifications(exc) invocation inside
the existing try block (or wrap it in its own try-catch) and on caught
exceptions produce a controlled error handling path (set an appropriate error
response on the Exchange and return the correct Outcome) so
expression-evaluation failures are logged and return a formatted 500-style
response instead of letting exceptions propagate.

In
`@core/src/main/java/com/predic8/membrane/core/lang/CommonBuiltInFunctions.java`:
- Around line 258-260: The urlEncode method currently calls URLEncoder.encode(s,
UTF_8) which throws NullPointerException for null inputs; update
CommonBuiltInFunctions.urlEncode to guard against null (e.g., if s is null
return "" like pathSeg does) before calling URLEncoder.encode using the UTF_8
constant so scripts won’t crash on null values.

In `@core/src/main/java/com/predic8/membrane/core/util/URLUtil.java`:
- Around line 24-30: The method getHost currently returns the authority
(host:port) rather than just the hostname; rename the method to getAuthority or
getHostPort (or update its javadoc) to reflect that behavior, update all call
sites to the new name, and if you need a pure hostname add a new method (e.g.,
getHostname) that strips any ":port" from the returned value; reference the
existing getHost method when making these changes so callers and documentation
stay consistent.
🧹 Nitpick comments (6)
core/src/main/java/com/predic8/membrane/core/http/Request.java (1)

172-183: Consider clearing body for all body-less methods, not just GET.

changeMethod only clears the body when switching to GET, but methodsWithoutBody (line 51) also includes HEAD and CONNECT. If the intent is to enforce body semantics on method change, the check could use the existing set:

if (methodsWithoutBody.contains(newMethod.toUpperCase()))
    emptyBody();

If only GET is intentional for the current use case, this is fine as-is — just flagging for consideration.

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

262-273: Minor Javadoc inconsistency: @param segment ... must not be null but URLUtil.pathSeg accepts null.

The underlying URLUtil.pathSeg(Object) gracefully handles null by returning "". The Javadoc here says "must not be null", which could confuse callers. Consider aligning the contract — either document that null returns empty, or add an explicit null check here.

core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java (1)

24-25: Redundant explicit URI import.

Line 24 imports java.net.* which already covers java.net.URI. Line 25's explicit import java.net.URI; is redundant.

Remove the redundant import
 import java.net.*;
-import java.net.URI;
core/src/main/java/com/predic8/membrane/core/proxies/Target.java (2)

60-67: Minor: asp.getTarget() is this — consider using direct field access for clarity.

applyModifications is an instance method called on asp.getTarget(), so asp.getTarget().getMethod() on Line 64 is equivalent to this.method. Using this.method != null and exc.getRequest().changeMethod(this.method) would be more direct and avoid the indirection.

Simplify self-reference
     public void applyModifications(Exchange exc, AbstractServiceProxy asp, Router router) {
         computeDestinationExpressions(exc, asp, router);
 
         // Changing the method must be the last step cause it can empty the body!
-        if (asp.getTarget().getMethod() != null) {
-            exc.getRequest().changeMethod(asp.getTarget().getMethod());
+        if (method != null) {
+            exc.getRequest().changeMethod(method);
         }
     }

69-78: Expression parsing happens per-request for each destination.

TemplateExchangeExpression.newInstance() creates a new instance for each destination URL on every request. For SPEL (the default language), this triggers SpelExpressionParser.parseExpression(), and for other languages, it parses tokens via regex matching. This occurs even for plain URLs containing no ${...} expressions.

Consider caching compiled expressions keyed by the raw URL string, or fast-pathing plain URLs that contain no expression markers.

core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java (1)

54-65: Minor inconsistency: these tests still instantiate new DefaultRouter() instead of using the shared router field.

The shared router field was introduced and used in protocolUpgradeRejected and computeTargetUrl, but these two tests still create their own instances. Consider reusing the shared field for consistency.

♻️ Proposed fix
     `@Test`
     void passFailOverOn500Default() {
-        hci.init(new DefaultRouter());
+        hci.init(router);
         assertFalse(hci.getHttpClientConfig().getRetryHandler().isFailOverOn5XX());
     }
 
     `@Test`
     void passFailOverOn500() {
         hci.setFailOverOn5XX(true);
-        hci.init(new DefaultRouter());
+        hci.init(router);
         assertTrue(hci.getHttpClientConfig().getRetryHandler().isFailOverOn5XX());
     }

Comment thread core/src/main/java/com/predic8/membrane/core/http/Message.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/URLUtil.java Outdated
…URL handling and modify target configurations

- Introduced `computeDestinationExpressions` to streamline target URL modification logic.
- Improved `DispatchingInterceptor` handling of base path resolution with illegal characters.
- Enhanced `urlEncode` and `pathSeg` utility functions for safety and clarity.
- Refactored `HTTPClientInterceptor` to adjust the target flow and error messages.
- Added extensive URI tests, including custom-parsed and relative path resolution scenarios.
- Updated `applyTargetModifications` for streamlined behavior during request processing.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@core/src/main/java/com/predic8/membrane/core/http/Request.java`:
- Around line 172-181: The changeMethod method can NPE when this.method or
newMethod is null; update changeMethod to defensively handle nulls by first
validating newMethod (either return or throw IllegalArgumentException if
newMethod is null), then compare using a null-safe check (e.g. if (this.method
!= null && this.method.equalsIgnoreCase(newMethod)) return) or invert to compare
newMethod.equalsIgnoreCase(this.method) after validating newMethod, and when
checking methodsWithoutBody avoid calling newMethod.toUpperCase() on a null (use
newMethod.toUpperCase(Locale.ROOT) only after null check or use
methodsWithoutBody.contains(newMethod.toUpperCase(Locale.ROOT)) guarded by
non-null); keep references to changeMethod, methodsWithoutBody, and emptyBody
when applying the fix.

In `@core/src/main/java/com/predic8/membrane/core/util/URI.java`:
- Around line 285-300: The custom-parsed resolve in URI.resolve can produce
"scheme://null..." when authority is null and also mishandles relative paths
that don't start with '/'; update the resolve(URI relative) method so that when
authority==null it emits scheme:... (no "://null") and so that relative paths
not starting with '/' are merged per RFC 3986 §5.2/5.3: compute basePath =
this.getRawPath(), strip everything after the last '/' (if any) and append the
relative raw path, ensuring the merged path has the correct leading '/'; then
append query if present (relative.getRawQuery()). Keep the existing behavior for
absolute relative paths (starting with '/') and reuse getRawPath/getRawQuery and
scheme/authority fields to construct the result for both cases. Ensure you
reference URI.resolve and the authority/scheme/getRawPath/getRawQuery fields
when making the change.

In `@core/src/main/java/com/predic8/membrane/core/util/URLUtil.java`:
- Around line 24-30: getAuthority currently assumes uri has characters past the
scheme colon and will call uri.charAt(i) without bounds checks, which can throw
StringIndexOutOfBoundsException for inputs like "" or ":" or "http://"; update
getAuthority to defensively validate uri and index i before accessing chars:
check for null/empty uri, ensure i < uri.length() before entering the while loop
and when calling uri.charAt(i), and handle the case where no authority exists
(e.g., return empty string or null) before computing j; reference getAuthority,
variables i and j, and the uri.charAt(i) loop when making the change.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/DispatchingInterceptorTest.java`:
- Around line 152-165: The test getAddressFromTargetElement currently only
prints the computed URL and has no assertion; update it to assert the expected
resolved address (for the Target URL "https://${property.host}:8080" with
exc.setProperty("host","predic8.de")) by calling
dispatcher.getAddressFromTargetElement(exc) and asserting it equals
"https://predic8.de:8080" (use the test framework's assertion method),
referencing the APIProxy/Target setup and the
dispatcher.getAddressFromTargetElement call so the test fails if resolution is
incorrect.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java`:
- Around line 67-82: The DispatchingInterceptor in the test is used without
initialization which can NPE in some cases; create the interceptor instance,
call init(router) on it before invoking handleRequest(exc) (e.g., var di = new
DispatchingInterceptor(); di.init(router); di.handleRequest(exc)), so the
interceptor has a router set just like hci.init(router) is done for
HTTPClientInterceptor; keep the rest of the test intact and ensure you call
di.init(router) prior to di.handleRequest.
🧹 Nitpick comments (1)
core/src/main/java/com/predic8/membrane/core/proxies/Target.java (1)

63-79: Clean separation of concerns — runtime expression evaluation.

The applyModifications method cleanly centralizes destination resolution and method override. The comment about method change ordering (Line 66) is helpful.

One consideration: TemplateExchangeExpression.newInstance is invoked per destination per request, which involves regex parsing and (for SpEL) expression compilation. For high-throughput proxies with plain target URLs (no ${}), this adds overhead on every request. If this becomes a bottleneck, consider short-circuiting when the URL contains no ${} placeholders.

Optional: skip template evaluation for plain URLs
     private String evaluateTemplate(Exchange exc, Router router, String url, InterceptorAdapter adapter) {
+        if (!url.contains("${")) {
+            return url;
+        }
         return TemplateExchangeExpression.newInstance(adapter, language, url, router).evaluate(exc, REQUEST, String.class);
     }

Comment thread core/src/main/java/com/predic8/membrane/core/http/Request.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/URI.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/URLUtil.java
…t URLs

- Introduced `TemplateUtil` with a method to check for template markers.
- Enhanced `TemplateExchangeExpression` to support URL encoding for dynamic templates.
- Updated `Target` to skip unnecessary evaluation for URLs without template markers.
- Added extensive tests to verify correct URL encoding across various languages (Groovy, SpEL, XPath, JSONPath).
… target URLs

- Documented the change to URL encode expressions inside `${}` in target URLs.
- Updated tests to improve validation coverage and logging for clearer output.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java`:
- Around line 79-95: In evaluateToString, guard against encoder.apply(null)
causing an NPE: after var value = token.eval(exchange, flow, String.class) check
if token is an Expression and value==null, and in that case substitute a safe
string (e.g., "" or "null" per project convention) before calling encoder.apply;
update the branch that currently does line.append(encoder.apply(value)) to
compute a safeValue = (value == null ? "" : value) and call
encoder.apply(safeValue) so null evaluations no longer cause a
NullPointerException.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java`:
- Around line 101-112: The test method computeTargetUrlWithEncodingXPath uses
post("/foo").json(...) while supplying XML; change the request builder call to
post("/foo").xml(...) so the Content-Type matches the XML body—update the
builder invocation in the test (computeTargetUrlWithEncodingXPath / variable
exc) to use .xml(...) instead of .json(...) to match other XPath tests and
maintain semantic consistency.
🧹 Nitpick comments (5)
core/src/test/java/com/predic8/membrane/core/interceptor/lang/SetHeaderInterceptorJsonpathTest.java (1)

25-26: Redundant import: assertTrue is already covered by the wildcard import on line 25.

Line 26 can be removed since import static org.junit.jupiter.api.Assertions.* already imports assertTrue.

Suggested fix
 import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.assertTrue;
core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java (1)

25-32: Null input will throw an unguarded NPE.

If null is ever passed (e.g., a destination URL that is null), s.length() will throw a NullPointerException with no helpful message. Consider adding an early null check or documenting the precondition.

Proposed defensive guard
     public static boolean containsTemplateMarker(String s) {
+        if (s == null) return false;
         for (int i = 0, len = s.length() - 1; i < len; i++) {
core/src/main/java/com/predic8/membrane/core/proxies/Target.java (1)

75-90: Template expressions are parsed and compiled on every request.

evaluateTemplate creates a new TemplateExchangeExpression (regex match + token compilation) for each destination URL on every invocation. For targets with template markers, this work is repeated per-request.

If target URLs with expressions are common or the gateway handles high throughput, consider caching the compiled TemplateExchangeExpression per unique URL string. That said, for typical use cases this is likely acceptable.

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

24-30: Consider adding a few more edge cases and splitting into named tests.

The current assertions cover the core scenarios well. A couple of additional boundary cases would strengthen confidence, especially a marker at the very start or end of the string, and a single $ character.

Suggested additional assertions
     `@Test`
     void test() {
         assertFalse(containsTemplateMarker(""));
         assertFalse(containsTemplateMarker("foo"));
         assertTrue(containsTemplateMarker("foo${bar"));
         assertFalse(containsTemplateMarker("foo$x{bar"));
+        assertTrue(containsTemplateMarker("${"));
+        assertFalse(containsTemplateMarker("$"));
+        assertTrue(containsTemplateMarker("end${"));
     }
core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java (1)

114-127: Rename auto-generated helper method extracted.

The name extracted is an IDE refactoring artifact and doesn't convey the method's purpose. A name like assertTargetUrlEncoding or verifyEncodedDestination would improve readability.

Suggested rename
-    private void extracted(Language language, Exchange exc, String url, String expected) {
+    private void assertTargetUrlEncoding(Language language, Exchange exc, String url, String expected) {

- Introduced a `templateExpressionCache` in `Target` to avoid redundant parsing and compilation of frequently used expressions.
- Improved token parsing and evaluation logic in `TemplateExchangeExpression`, enhancing clarity and performance.
- Renamed test helper method `extracted` to `testExpression` for better readability.
- Updated test cases across multiple classes to cover new enhancements and ensure consistent behavior.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@core/src/main/java/com/predic8/membrane/core/proxies/Target.java`:
- Around line 83-86: computeDestinationExpressions currently returns
Stream.toList() which produces an unmodifiable list that downstream code
mutates; change the terminal operation to use .collect(Collectors.toList()) so
the returned list is mutable. In computeDestinationExpressions, keep the
InterceptorAdapter(router, xmlConfig) and the evaluateTemplate(exc, router, url,
adapter) mapping but replace .toList() with .collect(Collectors.toList()) and
add the necessary import for java.util.stream.Collectors so
exc.setDestinations(...) and downstream interceptors (e.g., OpenAPIInterceptor,
InternalRoutingInterceptor, DestinationInterceptor, REST2SOAPInterceptor,
RewriteInterceptor) can safely modify the list.
- Line 60: The field templateExpressionCache in class Target is using a
non-thread-safe HashMap which can corrupt state when computeIfAbsent is invoked
concurrently; replace the HashMap with a java.util.concurrent.ConcurrentHashMap
(i.e., change the declaration of templateExpressionCache to new
ConcurrentHashMap<>()) and ensure any code referring to templateExpressionCache
(e.g., the computeIfAbsent usage inside Target) continues to work with the
concurrent map; also update imports as needed to use ConcurrentHashMap.
🧹 Nitpick comments (1)
core/src/test/java/com/predic8/membrane/core/util/TemplateUtilTest.java (1)

24-33: Consider more descriptive test method name and a few additional edge cases.

test() is quite generic. Consider splitting into or renaming to something like containsTemplateMarker() or using @ParameterizedTest for clarity.

A couple of additional edge cases worth covering:

  • A complete template expression: "${foo}"true
  • Null input: the current containsTemplateMarker implementation will throw an NPE on null. If that's intentional, a test documenting that (e.g., assertThrows(NullPointerException.class, ...)) would make the contract explicit.

Comment thread core/src/main/java/com/predic8/membrane/core/proxies/Target.java Outdated
Comment thread core/src/main/java/com/predic8/membrane/core/proxies/Target.java
…simplify logic

- Eliminated `templateExpressionCache` to reduce complexity and improve maintainability.
- Updated `computeDestinationExpressions` to use `Collectors.toList()` for a mutable list.
- Simplified `evaluateTemplate` by removing cache-related logic and comments on minor performance differences.
…n `Target`

- Added `Escaping` enum with options `NONE`, `URL`, and `SEGMENT` to enhance placeholder escaping in URLs.
- Updated `Target` to include configurable escaping behavior and a helper method `getEscapingFunction()`.
- Extended test cases to validate new escaping options in various scenarios.
Comment thread core/src/main/java/com/predic8/membrane/core/proxies/Target.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/URI.java
- Updated `URI` to leverage `var` for better readability and added JavaDoc for `resolve` method.
- Improved `ResolverMap` handling of URI schemes, including `classpath:` and `internal:`.
- Added new tests in `HTTPClientInterceptorTest` for complete path computation, including URL encoding.
@membrane-ci-server
Copy link
Copy Markdown

This pull request needs "/ok-to-test" from an authorized committer.

@predic8
Copy link
Copy Markdown
Member Author

predic8 commented Feb 16, 2026

/ok-to-test

…e template evaluation

- Added `UriIllegalCharacterDetector` for validating URI components with RFC 3986 compliance and optional extensions.
- Updated `Target` to skip template evaluation for URLs without placeholders and log warnings for insecure configurations.
- Enhanced `TemplateUtil.containsTemplateMarker` with null safety.
- Improved `RewriteInterceptor` to handle query parsing errors with more informative logging.
- Refactored tests and added new ones to validate changes.
- Refactored `removeLastSegment` method for concise logic using `Math.max()` to avoid redundant checks.
- Replaced `assertEquals(false, ...)` with `assertFalse(...)` for improved readability in tests.
- Cleaned up unused imports in `ResolverMapCombineTest`.
- Replaced `pathSeg` method with `pathEncode` for improved naming clarity and functionality in line with RFC 3986.
- Introduced `IPv6Util` for robust validation of IPv6 address text, ensuring only allowed characters are accepted.
- Refactored `Target` to include URL encoding strategy through a dedicated `escapingFunction` and removed redundant logic.
- Streamlined URI combination logic in `ResolverMap` with utility methods for handling slashes and file URIs.
- Added extensive tests covering URL path encoding, IPv6 address validation, and robustification of URI logic for edge cases.
… and utilities

- Adjusted visibility modifiers of constants to `final` for better immutability (`URITest`).
- Reordered equality assertion arguments for better readability (`TargetTest`).
- Replaced redundant comments with concise `<p/>` tags in JavaDocs for uniformity.
- Refactored tests to leverage `assertNull` for null comparisons (`FileUtilTest`).
- Removed unused method `stripIpv6BracketsIfNeeded` and redundant imports for cleaner code.
- Enhanced URI combination logic and streamlined utility imports (`DispatchingInterceptor`).
@predic8
Copy link
Copy Markdown
Member Author

predic8 commented Feb 18, 2026

@coderabbitai plz review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

@predic8, I'll review the changes in this pull request.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@predic8 predic8 requested a review from rrayst February 18, 2026 09:54
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (2)
core/src/main/java/com/predic8/membrane/core/util/URI.java (1)

192-198: ⚠️ Potential issue | 🔴 Critical

getHost() and getPort() will throw NullPointerException for URIs without an authority.

When authority is null (e.g., relative path "/foo"), processAuthority() returns early at line 93 without initializing hostPort. Calling getHost() (line 193) or getPort() (line 197) then dereferences null, causing a NullPointerException.

This path is exercised in tests (URITest.java line 615 creates URI("/foo", true)) and production code—for example, AbstractExchange.java line 314 calls new URI(getOriginalRequestUri()).getHost() without checking if authority exists.

Proposed null guard
 public String getHost() {
+    if (hostPort == null) return null;
     return hostPort.host;
 }

 public int getPort() {
+    if (hostPort == null) return -1;
     return hostPort.port;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/URI.java` around lines 192
- 198, The getters URI.getHost() and URI.getPort() can NPE because hostPort may
remain null when processAuthority() returns early for URIs without an authority;
update getHost() and getPort() to check if hostPort is null and return safe
defaults (null for host, -1 for port) instead of dereferencing hostPort, so
callers like new URI(...).getHost() won't throw; adjust any related
javadoc/comments on getPort()/getHost() to document these return values and
ensure processAuthority()/hostPort initialization need not change.
core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java (1)

99-111: ⚠️ Potential issue | 🟠 Major

Bug: URIFactory is lost during recursive combine with >2 locations.

When locations.length > 2, line 107 calls combine(combine(l), locations[...]) — the 2-argument combine(String...) overload on line 95, which hardcodes DEFAULT_URI_FACTORY. The factory parameter originally passed by the caller is silently discarded.

Although this code path is never triggered in the current codebase (all multi-location calls use the no-factory variant), the public API contract is violated. Any external caller invoking combine(customFactory, location1, location2, location3) would experience silent incorrect behavior.

Proposed fix — propagate factory through recursion
     private static String combineInternal(URIFactory factory,String... locations) {
         if (locations.length < 2)
             throw new InvalidParameterException();
 
         if (locations.length > 2) {
             // lfold
             String[] l = new String[locations.length - 1];
             System.arraycopy(locations, 0, l, 0, locations.length - 1);
-            return combine(combine(l), locations[locations.length - 1]);
+            return combine(factory, combine(factory, l), locations[locations.length - 1]);
         }
 
         return combineInternal2( factory, locations,locations[1], locations[0]);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java`
around lines 99 - 111, The recursive branch in combineInternal(URIFactory
factory,String... locations) loses the provided URIFactory by calling the
no-factory overload combine(combine(l), locations[last]) which uses
DEFAULT_URI_FACTORY; change the recursive call to propagate the original
factory, e.g. call the overload that accepts a URIFactory so the call becomes
combine(factory, combine(l), locations[locations.length-1]) (or equivalently
recurse via combineInternal with the factory), ensuring URIFactory is not
discarded; adjust references to combineInternal2 only where needed and keep
combineInternal, combine, combineInternal2 and URIFactory semantics intact.
🧹 Nitpick comments (12)
core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java (2)

17-17: Add a private no-arg constructor to prevent instantiation.

TemplateUtil is a pure-static utility class. Without an explicit private constructor, the compiler synthesises a public default one, allowing new TemplateUtil() at call sites.

♻️ Proposed fix
 public class TemplateUtil {
+
+    private TemplateUtil() {}
+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java` at line
17, TemplateUtil is a pure-static utility class and needs a private no-arg
constructor to prevent instantiation; add a private TemplateUtil() { }
(optionally throw new AssertionError("No instances")); to the TemplateUtil class
to stop the compiler from generating a public default constructor and ensure
callers cannot do new TemplateUtil().

25-33: Replace the manual loop with String.contains("${").

The comment claims "Fast implementation", but HotSpot's String.containsString.indexOf path uses native SIMD intrinsics and is at least as fast as the hand-rolled loop. Using contains is also clearer and eliminates the opportunity for an off-by-one mistake in future edits.

♻️ Proposed refactor
     public static boolean containsTemplateMarker(String s) {
-        if (s == null) return false;
-        for (int i = 0, len = s.length() - 1; i < len; i++) {
-            if (s.charAt(i) == '$' && s.charAt(i + 1) == '{') {
-                return true;
-            }
-        }
-        return false;
+        return s != null && s.contains("${");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java` around
lines 25 - 33, The containsTemplateMarker method currently uses a manual
char-by-char loop which is unnecessary and error-prone; replace its body to use
String.contains("${") (or s.indexOf("${") >= 0) to detect the marker, retaining
the null check and method name containsTemplateMarker so the change is localized
and clearer.
core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodeFalseArrayQueryParameterTest.java (1)

113-117: valuesUTF8 is misplaced in the Valid nested class.

The test now asserts an error condition (1 error with "Invalid query string" for raw non-ASCII characters), making it semantically identical to wrongCharacter in the Invalid class. It should be moved there and renamed to make its intent clear — e.g., rawUTF8InQueryStringIsInvalid. The companion test valuesAreDecoded already covers the valid (percent-encoded) UTF-8 case in Valid.

♻️ Proposed refactor: move and rename the test to `Invalid`

Remove from Valid:

-    `@Test`
-    void valuesUTF8() {
-        var err = validator.validate(get().path("/array?const=foo,äöü,baz"));
-        assertEquals(1, err.size());
-        assertTrue(err.get(0).getMessage().contains("Invalid query string"));
-    }

Add to the Invalid nested class:

+    `@Test`
+    void rawUTF8InQueryStringIsInvalid() {
+        var err = validator.validate(get().path("/array?const=foo,äöü,baz"));
+        assertEquals(1, err.size());
+        assertTrue(err.get(0).getMessage().contains("Invalid query string"));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodeFalseArrayQueryParameterTest.java`
around lines 113 - 117, The test method valuesUTF8 currently lives inside the
Valid nested class but asserts an error, so move it into the Invalid nested
class and rename it to rawUTF8InQueryStringIsInvalid (or similar) to make intent
clear; ensure its body remains the same (calling
validator.validate(get().path("/array?const=foo,äöü,baz")) and asserting a
single error containing "Invalid query string"), and keep the existing
valuesAreDecoded test in Valid unchanged to cover the percent-encoded UTF‑8
case; also check for any duplicate semantics with wrongCharacter and consolidate
if necessary.
core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodedArrayQueryParameterTest.java (1)

124-127: Exact error count and message string coupling may become fragile maintenance points.

Two minor concerns:

  1. assertEquals(1, err.size()) assumes the UriIllegalCharacterDetector emits exactly one error for the value äöü (3 non-ASCII codepoints). If the detector is ever changed to report per-character (producing 3 errors), this assertion silently becomes a regression rather than a spec.

  2. err.get(0).getMessage().contains("Illegal character") hard-couples the test to a specific English error message literal from the new detector. A message wording change or future i18n layer would silently break this test.

Consider extracting the expected message into a named constant shared with UriIllegalCharacterDetector, or at minimum using a less fragile substring:

💡 Suggested approach
+    // Pin to at least 1 error; exact count depends on per-value vs per-char reporting
-    assertEquals(1, err.size());
+    assertFalse(err.isEmpty());
     assertTrue(err.get(0).getMessage().contains("Illegal character"));

Or, if "exactly 1 per invalid parameter value" is an intentional contract, add a comment documenting that invariant so future authors know not to change the detector's reporting granularity without updating this test.

Also, var err here is the only use of var in the file; all other tests declare ValidationErrors err explicitly. Minor inconsistency, but worth aligning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodedArrayQueryParameterTest.java`
around lines 124 - 127, The test couples to implementation details: in
ExplodedArrayQueryParameterTest change the fragile assertions around
validator.validate(...) — replace the exact count assertion (assertEquals(1,
err.size())) with a less brittle check (e.g., assertTrue(err.size() >= 1) or
assertTrue(err.stream().anyMatch(...)) ) unless the contract of
UriIllegalCharacterDetector explicitly guarantees one error per parameter value
(in which case add a comment stating that invariant near the test); also avoid
hard‑coding the English message by comparing against a shared constant from
UriIllegalCharacterDetector or by using a language-agnostic check such as
matching a small stable substring or checking for the detector’s error code;
finally, replace the lone usage of var with the explicit type ValidationErrors
for consistency with other tests.
core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java (2)

60-75: validateHost — the explicit '.' check on line 67 is redundant.

'.' is already covered by isUnreserved(c) (line 32: c == '.'), so the c == '.' on line 67 is always true before reaching line 70. Not a bug — just a minor readability nit. Keeping it explicit is fine if the intent is documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java`
around lines 60 - 75, The check for '.' in validateHost is redundant because
isUnreserved(c) already returns true for '.', so remove the explicit c == '.'
branch in the character whitelist inside the validateHost method; update the
condition that currently reads if (c == '.' || c == '%' || c == ':' || c == '['
|| c == ']') to drop the '.' entry and keep the others, referencing the
validateHost method and isUnreserved helper to locate and verify the change.

17-17: Inconsistent utility class pattern — URIValidationUtil is not final and lacks a private constructor.

UriIllegalCharacterDetector is declared public final class with a private constructor to prevent instantiation. This class should follow the same pattern for consistency, since all methods are static.

Proposed fix
-public class URIValidationUtil {
+public final class URIValidationUtil {
+
+    private URIValidationUtil() {
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java` at
line 17, The utility class URIValidationUtil is mutable and instantiable but
only contains static methods; make it a proper utility by declaring the class
final and adding a private no-arg constructor (e.g., private URIValidationUtil()
{ } ) to prevent instantiation, leaving all existing static methods untouched;
mirror the pattern used in UriIllegalCharacterDetector to ensure consistency
across utility classes.
core/src/main/java/com/predic8/membrane/core/util/UriIllegalCharacterDetector.java (2)

17-19: Null safety: uri parameter is not null-checked, unlike options.

validateAll(URI uri, Options options) will throw an unguarded NullPointerException at uri.getScheme() if uri is null, whereas options is explicitly validated with Objects.requireNonNull in the overload at line 28. Consider adding a null check for consistency and clearer error messages.

Proposed fix
     public static void validateAll(URI uri, Options options) {
+        Objects.requireNonNull(uri, "uri");
         validateAll(uri.getScheme(), uri.getAuthority(), uri.getUserInfo(), uri.getRawPath(), uri.getRawQuery(), uri.getRawFragment(), options);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/util/UriIllegalCharacterDetector.java`
around lines 17 - 19, The validateAll(URI uri, Options options) overload lacks a
null check for uri and will NPE at uri.getScheme(); add an explicit null guard
(e.g., Objects.requireNonNull(uri, "uri must not be null")) at the start of the
method before calling uri.getScheme(), mirroring the options validation used in
the other validateAll overload to provide a clear error message and consistent
behavior.

58-84: Options class uses bare public mutable fields.

This works but makes it easy to accidentally mutate shared instances. If an Options object is ever cached or reused across threads, this could introduce subtle bugs. A builder or factory methods for common presets (e.g., Options.strict(), Options.relaxed()) would improve usability and safety.

Not urgent — fine as-is for an internal utility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/util/UriIllegalCharacterDetector.java`
around lines 58 - 84, The Options class exposes mutable public fields
(skipAllValidation, strictRfc3986, allowBracesInPath,
allowBracesInQueryAndFragment, allowBracesInUserInfo) which can cause accidental
shared-state bugs; make Options immutable and provide a builder or static
factory presets instead: convert fields to private final, add a nested
Options.Builder with fluent setters and a build() method, and add static factory
methods like Options.strict() and Options.relaxed() that return preconfigured
immutable instances; update any callers that construct Options to use the
builder or factory methods.
core/src/test/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptorTest.java (1)

102-103: Use JsonNode.path() to avoid NPE on absent fields.

JsonNode.get() returns null when the field is absent; chaining .asText().contains(…) on it will throw NullPointerException instead of a meaningful assertion failure. JsonNode.path() returns a MissingNode that safely returns an empty string from .asText().

♻️ Proposed fix
-       assertTrue(json.get("path").asText().contains("/buy/banana/%"));
-       assertTrue(json.get("component").asText().contains("rewrite"));
+       assertTrue(json.path("path").asText().contains("/buy/banana/%"));
+       assertTrue(json.path("component").asText().contains("rewrite"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptorTest.java`
around lines 102 - 103, Replace usages of JsonNode.get(...) with
JsonNode.path(...) in the assertions inside RewriteInterceptorTest (the two
assertions that check json.get("path").asText().contains(...) and
json.get("component").asText().contains(...)) so that missing fields produce a
safe MissingNode and yield empty strings instead of throwing NPE; update the two
expressions to use json.path("path").asText().contains(...) and
json.path("component").asText().contains(...) respectively to ensure safe
assertions.
core/src/main/java/com/predic8/membrane/core/proxies/Target.java (1)

116-139: Consider caching InterceptorAdapter if throughput profiling identifies this as a hot path.

The InterceptorAdapter constructor is lightweight (only field assignments), but a new instance is created per request per destination URL in computeDestinationExpressions(). While the inline comment correctly notes that caching showed negligible benefit, if high-throughput profiling later reveals this as a bottleneck, the adapter could be pre-built once in the init() method and stored as a field, assuming the same router and xmlConfig instances remain stable throughout the application lifecycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/proxies/Target.java` around
lines 116 - 139, computeDestinationExpressions currently instantiates a new
InterceptorAdapter for every call which may be a hot path; to mitigate this,
create and reuse a single InterceptorAdapter instance by initializing it once
(e.g. in init() or as a lazy final field) instead of inside
computeDestinationExpressions, ensuring it is constructed with the same router
and xmlConfig used today; update computeDestinationExpressions to use the cached
InterceptorAdapter and keep the existing behavior of evaluateTemplate and
evaluateExpressions unchanged, and ensure thread-safety if fields may be
accessed concurrently.
core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java (1)

152-182: deactivateEvaluationOfURLTemplatesWhenIllegalCharactersAreAllowed and uriTemplateAndIllegalCharacters appear to test the same code path.

Both tests:

  1. Call allowIllegalURICharacters()
  2. Use the same target URL "https://${'hostname'}" (template expression)
  3. Assert ConfigurationException

The only difference is the request URI ("/foo/${555}" vs "/foo"). If ConfigurationException originates from target.init(router) (before dispatching even inspects the request URI), the request URI content is irrelevant and both tests exercise the same branch.

Either add a comment clarifying what distinct scenario the ${555}-URI variant tests, or consolidate the two into one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java`
around lines 152 - 182, These two tests
(deactivateEvaluationOfURLTemplatesWhenIllegalCharactersAreAllowed and
uriTemplateAndIllegalCharacters) exercise the same failure path because both
call allowIllegalURICharacters() and use the template target
"https://${'hostname'}" which fails at target.init(router) before the request
URI matters; either consolidate them into a single test that asserts
ConfigurationException for invokeDispatching(SPEL, ..., "https://${'hostname'}",
Escaping.URL) and remove the duplicate, or change
uriTemplateAndIllegalCharacters to a different distinct scenario (e.g., use a
non-template target or assert different post-init behavior) so it no longer
duplicates deactivateEvaluationOfURLTemplatesWhenIllegalCharactersAreAllowed;
update or add a brief comment in the remaining test to explain why the request
URI does or does not affect the failure and reference
allowIllegalURICharacters(), invokeDispatching(...), and the two test method
names to locate the code.
core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java (1)

53-55: count field has package-private visibility — consider making it private.

count, schemas, and resolvers are internal state of ResolverMap. count in particular lacks the private modifier (lines 53), which exposes it to other classes in the same package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java`
around lines 53 - 55, The field count in class ResolverMap is package-private;
make it private to encapsulate internal state along with schemas and resolvers;
update the declaration of count to private int count and ensure any direct
accesses from ResolverMap methods (or other classes) use existing ResolverMap
methods (or add getters/setters if needed) so external code no longer relies on
the package-visible field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/main/java/com/predic8/membrane/core/util/URI.java`:
- Around line 272-279: getWithoutPath() blindly concatenates getScheme() + "://"
+ getAuthority(), producing "null://null" for relative or authority-less URIs;
to fix, check getScheme() and getAuthority() at the start of getWithoutPath():
if either is null throw an IllegalStateException (or return a documented
alternative like the original URI string) with a clear message; update the
javadoc to state the method only applies to absolute hierarchical URIs. Ensure
references to getScheme(), getAuthority(), and getWithoutPath() remain in the
message so callers can locate the contract.

In `@core/src/main/java/com/predic8/membrane/core/util/uri/EscapingUtil.java`:
- Around line 30-32: The Javadoc for the URL escaping strategy is wrong:
URLEncoder.encode (used in EscapingUtil's URL strategy) produces '+' for spaces,
not '%20'; update the implementation in EscapingUtil's URL strategy to
post-process the result of URLEncoder.encode(...) by replacing '+' with "%20"
(and leave other percent-encodings intact) so spaces are percent-encoded for
URI/path contexts, and also update the Javadoc text for the URL strategy to
reflect that spaces become "%20" (or, if you prefer not to change behavior,
instead update the Javadoc to state that URLEncoder yields '+'); reference the
URL strategy, the use of URLEncoder.encode, and EscapingUtil when making the
change.
- Around line 69-73: Update the Javadoc examples in EscapingUtil to reference
the correct method name pathEncode instead of the stale pathSeg; specifically
replace occurrences like pathSeg("a/b & c") and pathSeg("ä") and pathSeg(123)
with pathEncode("a/b & c"), pathEncode("ä"), and pathEncode(123) in the Javadoc
block so the examples match the actual method name.

In `@core/src/main/java/com/predic8/membrane/core/util/URIFactory.java`:
- Around line 27-28: The public static singletons DEFAULT_URI_FACTORY and
ALLOW_ILLEGAL_CHARACTORY_FACTORY expose mutable URIFactory state; prevent global
mutation by making those instances immutable: create an unmodifiable subclass
(e.g., new URIFactory(true) { `@Override` setAllowIllegalCharacters(...){ throw
new UnsupportedOperationException(); } `@Override` setAutoEscapeBackslashes(...){
throw new UnsupportedOperationException(); } }) for
ALLOW_ILLEGAL_CHARACTERS_URI_FACTORY (and a similar immutable instance for
DEFAULT_URI_FACTORY), or alternatively stop exposing mutable instances and
instead expose factory methods that return new URIFactory copies; update
references to use the new immutable instances or callers that obtain copies and
remove/disable setter behavior on the shared instances (target methods:
setAllowIllegalCharacters, setAutoEscapeBackslashes, any other URIFactory
setters).

In `@core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java`:
- Around line 19-24: validateDigits currently throws a NullPointerException when
port is null; update validateDigits in URIValidationUtil to guard against null
(either return/ignore null inputs or throw a clear IllegalArgumentException)
before calling port.length(), consistent with how UriIllegalCharacterDetector
handles nulls; ensure the chosen behavior is documented in the method javadoc
and use the method name validateDigits as the spot to add the null check and
error handling so callers get a clear, intentional outcome instead of an NPE.

In `@core/src/main/java/com/predic8/membrane/core/util/URLUtil.java`:
- Around line 46-50: The getNameComponent method may call getPath() on an opaque
URI and get null; update getNameComponent(URIFactory uriFactory, String uri) to
capture the result of uriFactory.create(uri).getPath() into a local variable,
check for null, and handle it (e.g., return an empty string or throw a
descriptive URISyntaxException) before computing lastIndexOf('/') and substring;
ensure references to getNameComponent and uriFactory.create(uri).getPath() are
used so reviewers can find the change.

In `@core/src/test/java/com/predic8/membrane/core/openapi/util/UriUtilTest.java`:
- Around line 86-95: Rename the misleading test methods to reflect they start
from a fully-qualified http URL: change the method rewriteWithoutHttp() to
rewriteFromHttpToHttp() and rewriteWithoutHttps() to rewriteFromHttpToHttps()
(keep the `@Test` annotation and method signatures the same except for the names)
and update any references or imports that call these method names so test
discovery and naming match the intent; verify the assertions inside
doRewrite(...) remain unchanged.

In `@core/src/test/java/com/predic8/membrane/core/util/FileUtilTest.java`:
- Around line 54-61: The Linux-only assertions in the FileUtilTest.resolveFile
test hard-code POSIX paths; update the expectations to be OS-aware by wrapping
the expected URI strings with OSUtil.wl so they match Windows drive-letter
absolute paths too. Add a static import for wl (e.g. import static
com.predic8.membrane.core.util.OSUtil.*) in the FileUtilTest and replace the
literal expected values in the resolveFile assertions (which reference
FileUtil.resolve) with wl("file:/a/b/c/d"), wl("file:/a/b/c/d/"), etc., so the
test passes on all platforms.

In
`@core/src/test/java/com/predic8/membrane/core/util/uri/URIVsJavaNetURITest.java`:
- Line 65: The test's assertion message is misleading: update the assertion in
URIVsJavaNetURITest (the assertEquals comparing j.getHost() and custom.getHost()
where variables j and custom are java.net.URI and custom URI) to reflect that
java.net.URI#getHost() returns IPv6 hosts with brackets; replace the message
"host (java host is bracket-free)" with a correct message such as "host (java
host includes brackets)" or remove the parenthetical entirely so the assertion
message is accurate.

In `@core/src/test/java/com/predic8/membrane/core/util/URITest.java`:
- Around line 649-653: In the URITest class update the inline comment that
currently reads "getRessource() can deal with that" to the correct spelling
"getResource() can deal with that" (edit the comment near the assertEquals in
URITest where base.resolve(relative) is asserted) so the method name is
correctly spelled.
- Around line 139-141: The three identical assertions in URITest all call
uf.create("http://localhost/foo").getWithoutPath(); locate the test method in
URITest and remove the duplicate assertions or replace two of them with
meaningful variations (e.g., use uf.create with a query string like
"http://localhost/foo?bar=1" and with a fragment like
"http://localhost/foo#frag") so that tests still assert getWithoutPath() returns
"http://localhost" for different input forms; update or delete the extra
assertEquals calls accordingly.

---

Outside diff comments:
In `@core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java`:
- Around line 99-111: The recursive branch in combineInternal(URIFactory
factory,String... locations) loses the provided URIFactory by calling the
no-factory overload combine(combine(l), locations[last]) which uses
DEFAULT_URI_FACTORY; change the recursive call to propagate the original
factory, e.g. call the overload that accepts a URIFactory so the call becomes
combine(factory, combine(l), locations[locations.length-1]) (or equivalently
recurse via combineInternal with the factory), ensuring URIFactory is not
discarded; adjust references to combineInternal2 only where needed and keep
combineInternal, combine, combineInternal2 and URIFactory semantics intact.

In `@core/src/main/java/com/predic8/membrane/core/util/URI.java`:
- Around line 192-198: The getters URI.getHost() and URI.getPort() can NPE
because hostPort may remain null when processAuthority() returns early for URIs
without an authority; update getHost() and getPort() to check if hostPort is
null and return safe defaults (null for host, -1 for port) instead of
dereferencing hostPort, so callers like new URI(...).getHost() won't throw;
adjust any related javadoc/comments on getPort()/getHost() to document these
return values and ensure processAuthority()/hostPort initialization need not
change.

---

Duplicate comments:
In
`@core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java`:
- Around line 91-94: In TemplateExchangeExpression (class
TemplateExchangeExpression) remove the fallback that appends the literal "null"
when a resolved expression value is null and instead throw an
ExchangeExpressionException indicating the specific expression that evaluated to
null; update the null branch inside the loop (the block handling value == null)
to throw new ExchangeExpressionException with a clear message referencing the
expression (e.g., the template token or expression string) so callers fail fast
and can handle the error.

In `@core/src/main/java/com/predic8/membrane/core/util/URLUtil.java`:
- Around line 21-27: The getAuthority method can index past the string end;
before using uri.charAt(i) add defensive checks: first verify colon exists
(uri.indexOf(":") >= 0) and that i < uri.length() after computing i, and change
the while to require i < uri.length() && uri.charAt(i) == '/'; also handle cases
where i reaches uri.length() (e.g. return "" or null) and keep the existing j =
uri.indexOf("/", i) logic but ensure j is computed only when i < uri.length();
update getAuthority to return a safe empty string for malformed inputs instead
of allowing StringIndexOutOfBoundsException.

---

Nitpick comments:
In `@core/src/main/java/com/predic8/membrane/core/proxies/Target.java`:
- Around line 116-139: computeDestinationExpressions currently instantiates a
new InterceptorAdapter for every call which may be a hot path; to mitigate this,
create and reuse a single InterceptorAdapter instance by initializing it once
(e.g. in init() or as a lazy final field) instead of inside
computeDestinationExpressions, ensuring it is constructed with the same router
and xmlConfig used today; update computeDestinationExpressions to use the cached
InterceptorAdapter and keep the existing behavior of evaluateTemplate and
evaluateExpressions unchanged, and ensure thread-safety if fields may be
accessed concurrently.

In `@core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java`:
- Around line 53-55: The field count in class ResolverMap is package-private;
make it private to encapsulate internal state along with schemas and resolvers;
update the declaration of count to private int count and ensure any direct
accesses from ResolverMap methods (or other classes) use existing ResolverMap
methods (or add getters/setters if needed) so external code no longer relies on
the package-visible field.

In `@core/src/main/java/com/predic8/membrane/core/util/TemplateUtil.java`:
- Line 17: TemplateUtil is a pure-static utility class and needs a private
no-arg constructor to prevent instantiation; add a private TemplateUtil() { }
(optionally throw new AssertionError("No instances")); to the TemplateUtil class
to stop the compiler from generating a public default constructor and ensure
callers cannot do new TemplateUtil().
- Around line 25-33: The containsTemplateMarker method currently uses a manual
char-by-char loop which is unnecessary and error-prone; replace its body to use
String.contains("${") (or s.indexOf("${") >= 0) to detect the marker, retaining
the null check and method name containsTemplateMarker so the change is localized
and clearer.

In
`@core/src/main/java/com/predic8/membrane/core/util/UriIllegalCharacterDetector.java`:
- Around line 17-19: The validateAll(URI uri, Options options) overload lacks a
null check for uri and will NPE at uri.getScheme(); add an explicit null guard
(e.g., Objects.requireNonNull(uri, "uri must not be null")) at the start of the
method before calling uri.getScheme(), mirroring the options validation used in
the other validateAll overload to provide a clear error message and consistent
behavior.
- Around line 58-84: The Options class exposes mutable public fields
(skipAllValidation, strictRfc3986, allowBracesInPath,
allowBracesInQueryAndFragment, allowBracesInUserInfo) which can cause accidental
shared-state bugs; make Options immutable and provide a builder or static
factory presets instead: convert fields to private final, add a nested
Options.Builder with fluent setters and a build() method, and add static factory
methods like Options.strict() and Options.relaxed() that return preconfigured
immutable instances; update any callers that construct Options to use the
builder or factory methods.

In `@core/src/main/java/com/predic8/membrane/core/util/URIValidationUtil.java`:
- Around line 60-75: The check for '.' in validateHost is redundant because
isUnreserved(c) already returns true for '.', so remove the explicit c == '.'
branch in the character whitelist inside the validateHost method; update the
condition that currently reads if (c == '.' || c == '%' || c == ':' || c == '['
|| c == ']') to drop the '.' entry and keep the others, referencing the
validateHost method and isUnreserved helper to locate and verify the change.
- Line 17: The utility class URIValidationUtil is mutable and instantiable but
only contains static methods; make it a proper utility by declaring the class
final and adding a private no-arg constructor (e.g., private URIValidationUtil()
{ } ) to prevent instantiation, leaving all existing static methods untouched;
mirror the pattern used in UriIllegalCharacterDetector to ensure consistency
across utility classes.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptorTest.java`:
- Around line 152-182: These two tests
(deactivateEvaluationOfURLTemplatesWhenIllegalCharactersAreAllowed and
uriTemplateAndIllegalCharacters) exercise the same failure path because both
call allowIllegalURICharacters() and use the template target
"https://${'hostname'}" which fails at target.init(router) before the request
URI matters; either consolidate them into a single test that asserts
ConfigurationException for invokeDispatching(SPEL, ..., "https://${'hostname'}",
Escaping.URL) and remove the duplicate, or change
uriTemplateAndIllegalCharacters to a different distinct scenario (e.g., use a
non-template target or assert different post-init behavior) so it no longer
duplicates deactivateEvaluationOfURLTemplatesWhenIllegalCharactersAreAllowed;
update or add a brief comment in the remaining test to explain why the request
URI does or does not affect the failure and reference
allowIllegalURICharacters(), invokeDispatching(...), and the two test method
names to locate the code.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptorTest.java`:
- Around line 102-103: Replace usages of JsonNode.get(...) with
JsonNode.path(...) in the assertions inside RewriteInterceptorTest (the two
assertions that check json.get("path").asText().contains(...) and
json.get("component").asText().contains(...)) so that missing fields produce a
safe MissingNode and yield empty strings instead of throwing NPE; update the two
expressions to use json.path("path").asText().contains(...) and
json.path("component").asText().contains(...) respectively to ensure safe
assertions.

In
`@core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodedArrayQueryParameterTest.java`:
- Around line 124-127: The test couples to implementation details: in
ExplodedArrayQueryParameterTest change the fragile assertions around
validator.validate(...) — replace the exact count assertion (assertEquals(1,
err.size())) with a less brittle check (e.g., assertTrue(err.size() >= 1) or
assertTrue(err.stream().anyMatch(...)) ) unless the contract of
UriIllegalCharacterDetector explicitly guarantees one error per parameter value
(in which case add a comment stating that invariant near the test); also avoid
hard‑coding the English message by comparing against a shared constant from
UriIllegalCharacterDetector or by using a language-agnostic check such as
matching a small stable substring or checking for the detector’s error code;
finally, replace the lone usage of var with the explicit type ValidationErrors
for consistency with other tests.

In
`@core/src/test/java/com/predic8/membrane/core/openapi/oas31/parameters/ExplodeFalseArrayQueryParameterTest.java`:
- Around line 113-117: The test method valuesUTF8 currently lives inside the
Valid nested class but asserts an error, so move it into the Invalid nested
class and rename it to rawUTF8InQueryStringIsInvalid (or similar) to make intent
clear; ensure its body remains the same (calling
validator.validate(get().path("/array?const=foo,äöü,baz")) and asserting a
single error containing "Invalid query string"), and keep the existing
valuesAreDecoded test in Valid unchanged to cover the percent-encoded UTF‑8
case; also check for any duplicate semantics with wrongCharacter and consolidate
if necessary.

Comment thread core/src/main/java/com/predic8/membrane/core/util/URI.java
Comment thread core/src/main/java/com/predic8/membrane/core/util/uri/EscapingUtil.java Outdated
Comment thread core/src/main/java/com/predic8/membrane/core/util/uri/EscapingUtil.java Outdated
Comment thread core/src/main/java/com/predic8/membrane/core/util/URIFactory.java
Comment thread core/src/test/java/com/predic8/membrane/core/openapi/util/UriUtilTest.java Outdated
Comment thread core/src/test/java/com/predic8/membrane/core/util/FileUtilTest.java
Comment thread core/src/test/java/com/predic8/membrane/core/util/uri/URIVsJavaNetURITest.java Outdated
Comment thread core/src/test/java/com/predic8/membrane/core/util/URITest.java Outdated
Comment thread core/src/test/java/com/predic8/membrane/core/util/URITest.java
…comprehensive RFC 3986 compliance tests

- Added `URIFactory` to support flexible URI creation with options for illegal character handling and backslash escaping.
- Implemented extensive
… parsing

- Added edge case assertions for parsing IPv6 with empty ports.
- Introduced additional checks for IPv4 host and port parsing scenarios.
- Renamed test methods to reflect broader case coverage.
Comment thread distribution/examples/configuration/apis.yaml Outdated
…Target` and `CallInterceptor`

- Replaced `evaluateExpressions` with `urlIsTemplate` for better semantics and clarity.
- Improved `Target` and `CallInterceptor` to skip URL evaluation when no template markers are present.
- Introduced stricter validation for configurations involving template markers and illegal characters.
- Updated relevant tests to validate changes and added new test cases for URL template evaluation.
@predic8 predic8 requested a review from rrayst February 19, 2026 10:39
@rrayst rrayst merged commit 54c7e3b into master Feb 20, 2026
5 checks passed
@rrayst rrayst deleted the compute-url-expression-at-http-client-interceptor branch February 20, 2026 08:20
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