This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 6167c0bd2c51 CAMEL-24524: camel-util - Fix URISupport.normalizeUri
fast path producing different output depending on original parameter order
(#25810)
6167c0bd2c51 is described below
commit 6167c0bd2c510e8e56e7f644617064d68b82de47
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Aug 28 15:29:23 2026 +0200
CAMEL-24524: camel-util - Fix URISupport.normalizeUri fast path producing
different output depending on original parameter order (#25810)
* CAMEL-24524: camel-util - Fix URISupport.normalizeUri fast path producing
different output depending on original parameter order
The fast normalizer's buildReorderingParameters() only rebuilt (and thereby
encoded) the query string when the parameter keys were not already in
alphabetical order. Since rebuilding was the only place encoding happened,
two logically identical endpoint URIs differing only in original parameter
order could normalize to different strings whenever a value needed encoding
(eg a colon in a host:port value). As normalizeUri() is used to compute the
endpoint registry key, this could silently create duplicate endpoints.
Always rebuild the query so the result no longer depends on incidental
parameter order, matching the behavior already used by the complex
normalizer path.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-24524: camel-util - Use RFC 3986-safe encoding when rebuilding
normalized query strings
buildReorderingParameters always rebuilds the query to fix order-dependent
output, but rebuilding via createQueryString/URLEncoder needlessly percent-
encoded characters that are legal unescaped in a URI query (eg ':' in
host:port values, '/' in MIME types such as produces=application/json,
which broke RestOpenApiEndpointV3Test).
Switch to UnsafeUriCharactersEncoder, Camel's existing lightweight URI-safe
escaper, additionally escaping '&' and '=' since those remain structurally
significant in Camel's key=value&key=value query syntax. Updated the
existing order-independence tests to reflect the less aggressive encoding
and added a regression test for the slash case.
Co-Authored-By: Claude Code on behalf of Claus Ibsen <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-24524: docs - Add 4.23 upgrade guide note for endpoint URI
normalization fix
Co-Authored-By: Claude Code on behalf of Claus Ibsen <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.../java/org/apache/camel/util/URISupport.java | 83 ++++++++++++------
.../java/org/apache/camel/util/URISupportTest.java | 96 ++++++++++++++++++++-
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 97 +++++++++++++---------
3 files changed, 212 insertions(+), 64 deletions(-)
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
index 8ab513173e17..25c658967178 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
@@ -797,38 +797,73 @@ public final class URISupport {
}
private static String buildReorderingParameters(String scheme, String
path, String query) throws URISyntaxException {
- Map<String, Object> parameters = null;
- if (query.indexOf('&') != -1) {
- // only parse if there are parameters
- parameters = URISupport.parseQuery(query, false, false);
+ Map<String, Object> parameters = URISupport.parseQuery(query, false,
false);
+
+ final String[] keys = parameters.keySet().toArray(new String[0]);
+ if (keys.length > 1) {
+ // reorder parameters a..z
+ Arrays.sort(keys);
+ }
+ // always rebuild the query, even if the keys were already in order,
so the output does not
+ // depend on the incidental original parameter order. Escape only the
characters that are
+ // genuinely unsafe in a URI (as UnsafeUriCharactersEncoder does
elsewhere in Camel) rather than
+ // the much more aggressive application/x-www-form-urlencoded encoding
used by
+ // createQueryString()/URLEncoder, which would needlessly
percent-encode characters that are
+ // legal unescaped in a URI query, such as ':' (eg host:port) or '/'
(eg produces=application/json)
+ query = buildSafeQueryString(keys, parameters);
+ return buildUri(scheme, path, query);
+ }
+
+ private static String buildSafeQueryString(String[] sortedKeys,
Map<String, Object> parameters) {
+ if (parameters.isEmpty()) {
+ return EMPTY_QUERY_STRING;
}
- if (parameters != null && parameters.size() != 1) {
- final Set<String> entries = parameters.keySet();
+ StringBuilder sb = new StringBuilder(128);
+ boolean first = true;
+ for (String key : sortedKeys) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append('&');
+ }
- // reorder parameters a..z
- // optimize and only build new query if the keys was resorted
- boolean sort = false;
- String prev = null;
- for (String key : entries) {
- if (prev != null) {
- int comp = key.compareTo(prev);
- if (comp < 0) {
- sort = true;
- break;
+ Object value = parameters.get(key);
+ if (value instanceof List) {
+ List<String> list = (List<String>) value;
+ for (Iterator<String> it = list.iterator(); it.hasNext();) {
+ appendSafeQueryStringParameter(key, it.next(), sb);
+ if (it.hasNext()) {
+ sb.append('&');
}
}
- prev = key;
- }
- if (sort) {
- final String[] array = entries.toArray(new String[0]);
- Arrays.sort(array);
-
- query = URISupport.createQueryString(array, parameters, true);
+ } else {
+ String s = value != null ? value.toString() : null;
+ appendSafeQueryStringParameter(key, s, sb);
}
+ }
+ return sb.toString();
+ }
+ private static void appendSafeQueryStringParameter(String key, String
value, StringBuilder sb) {
+ sb.append(key);
+ if (value == null) {
+ return;
+ }
+ sb.append('=');
+ String raw = URIScanner.resolveRaw(value);
+ if (raw != null) {
+ // do not encode RAW parameters unless it has %
+ // need to replace % with %25 to avoid losing "%" when decoding
+ sb.append(URIScanner.replacePercent(value));
+ } else {
+ // '&' and '=' are structurally significant in Camel's
key=value&key=value query syntax
+ // and must stay escaped inside a value even though they are
otherwise legal, unescaped
+ // characters in a URI query per RFC 3986 -
UnsafeUriCharactersEncoder does not escape them
+ // as it is also used outside of this query-value context
+ String encoded =
UnsafeUriCharactersEncoder.encode(value).replace("&", "%26").replace("=",
"%3D");
+ sb.append(encoded);
}
- return buildUri(scheme, path, query);
}
private static String buildUri(String scheme, String path, String query) {
diff --git
a/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
b/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
index ac01d6891a94..489bf6741f18 100644
--- a/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
+++ b/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
@@ -197,8 +197,9 @@ public class URISupportTest {
public void testNormalizeEndpointWithEqualSignInParameter() throws
Exception {
String out =
URISupport.normalizeUri("jms:queue:foo?selector=somekey='somevalue'&foo=bar");
assertNotNull(out);
- // Camel will safe encode the URI
-
assertEquals("jms://queue:foo?foo=bar&selector=somekey%3D%27somevalue%27", out);
+ // Camel will safe encode the URI - '=' stays escaped as it is
structurally significant in
+ // the query syntax, but the single quotes (legal unescaped in a URI
query) are left as-is
+ assertEquals("jms://queue:foo?foo=bar&selector=somekey%3D'somevalue'",
out);
}
@Test
@@ -261,6 +262,97 @@ public class URISupportTest {
assertEquals(out1, out2);
}
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentWithColonValue()
throws Exception {
+ // CAMEL-24524: a value containing a colon (eg host:port) must
normalize the same way
+ // regardless of whether the original parameter order already happened
to be alphabetical.
+ // ':' is legal unescaped in a URI query (RFC 3986 pchar) so it must
not be percent-encoded.
+ String out1 =
URISupport.normalizeUri("kafka:mytopic?brokers=localhost:19092&groupId=mygroup");
+ String out2 =
URISupport.normalizeUri("kafka:mytopic?groupId=mygroup&brokers=localhost:19092");
+
+ assertThat(out1).isEqualTo(out2);
+
assertThat(out1).isEqualTo("kafka://mytopic?brokers=localhost:19092&groupId=mygroup");
+ }
+
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentWithThreeParameters()
throws Exception {
+ // all 6 permutations of 3 keys (one already alphabetical, some not)
must normalize identically
+ String[] permutations = new String[] {
+
"kafka:mytopic?brokers=localhost:19092&groupId=mygroup&clientId=myclient",
+
"kafka:mytopic?brokers=localhost:19092&clientId=myclient&groupId=mygroup",
+
"kafka:mytopic?groupId=mygroup&brokers=localhost:19092&clientId=myclient",
+
"kafka:mytopic?groupId=mygroup&clientId=myclient&brokers=localhost:19092",
+
"kafka:mytopic?clientId=myclient&brokers=localhost:19092&groupId=mygroup",
+
"kafka:mytopic?clientId=myclient&groupId=mygroup&brokers=localhost:19092" };
+
+ String expected =
"kafka://mytopic?brokers=localhost:19092&clientId=myclient&groupId=mygroup";
+ for (String uri : permutations) {
+ assertThat(URISupport.normalizeUri(uri)).as("normalizing: " +
uri).isEqualTo(expected);
+ }
+ }
+
+ @Test
+ public void
testNormalizeEndpointUriOrderIndependentSingleParameterWithColonValue() throws
Exception {
+ // CAMEL-24524: the single-parameter shortcut must also normalize the
value consistently
+ String out =
URISupport.normalizeUri("kafka:mytopic?brokers=localhost:19092");
+ assertThat(out).isEqualTo("kafka://mytopic?brokers=localhost:19092");
+ }
+
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentWithCommaValue()
throws Exception {
+ // a value with a comma (safe for the fast parser, and legal unescaped
in a URI query)
+ // must also normalize the same regardless of key order, without being
percent-encoded
+ String out1 =
URISupport.normalizeUri("smtp://localhost?subject=Hello,World&username=davsclaus");
+ String out2 =
URISupport.normalizeUri("smtp://localhost?username=davsclaus&subject=Hello,World");
+
+ assertThat(out1).isEqualTo(out2);
+
assertThat(out1).isEqualTo("smtp://localhost?subject=Hello,World&username=davsclaus");
+ }
+
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentWithSlashValue()
throws Exception {
+ // CAMEL-24524 follow-up: a value with a slash (eg a MIME type such as
+ // produces=application/json, as built by camel-rest-openapi's
RestOpenApiEndpoint) must not
+ // be percent-encoded - '/' is legal unescaped in a URI query (RFC
3986 pchar). This previously
+ // broke
RestOpenApiEndpointV3Test#shouldNotCrossContaminateProducersForSameOperation
once the
+ // fast-path normalizer started always re-encoding the query, even
when the keys ("host",
+ // "produces") were already in alphabetical order.
+ String out1 =
URISupport.normalizeUri("foo:bar?host=http://petstore.example.com&produces=application/json");
+ String out2 =
URISupport.normalizeUri("foo:bar?produces=application/json&host=http://petstore.example.com");
+
+ assertThat(out1).isEqualTo(out2);
+
assertThat(out1).isEqualTo("foo://bar?host=http://petstore.example.com&produces=application/json");
+ }
+
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentIsIdempotent() throws
Exception {
+ // normalizing an already-normalized uri must return the exact same
string
+ String out1 =
URISupport.normalizeUri("kafka:mytopic?groupId=mygroup&brokers=localhost:19092");
+ String out2 = URISupport.normalizeUri(out1);
+
+ assertThat(out2).isEqualTo(out1);
+ }
+
+ @Test
+ public void testNormalizeEndpointUriOrderIndependentWithRawValue() throws
Exception {
+ // RAW() values must not be further encoded, regardless of key order
+ String out1 =
URISupport.normalizeUri("kafka:mytopic?password=RAW(p@ss:word)&username=scott");
+ String out2 =
URISupport.normalizeUri("kafka:mytopic?username=scott&password=RAW(p@ss:word)");
+
+ assertThat(out1).isEqualTo(out2);
+
assertThat(out1).isEqualTo("kafka://mytopic?password=RAW(p@ss:word)&username=scott");
+ }
+
+ @Test
+ public void
testNormalizeEndpointUriOrderIndependentWithDualParametersAndColonValue()
throws Exception {
+ // duplicate keys (list values) combined with a colon value, which is
legal unescaped
+ String out1 =
URISupport.normalizeUri("smtp://localhost?to=foo:1&to=bar:2&from=me");
+ String out2 =
URISupport.normalizeUri("smtp://localhost?from=me&to=foo:1&to=bar:2");
+
+ assertThat(out1).isEqualTo(out2);
+
assertThat(out1).isEqualTo("smtp://localhost?from=me&to=foo:1&to=bar:2");
+ }
+
@Test
public void testSanitizeAccessToken() {
String out1 = URISupport
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index e072b59ab56a..762d40c1acfc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -143,6 +143,65 @@ The component camel-threadpoolfactory-vertx was deprecated
in 4.21. The componen
`camel-zeebe` component was deprecated in 4.19 and has a straightforward
replacement with `camel-camunda`. It is removed in 4.23.
+=== camel-core - endpoint URI normalization is now order-independent
+
+`URISupport.normalizeUri()` computes the endpoint registry cache key so that
two logically identical
+endpoint URIs (differing only in query parameter order) resolve to a single
shared `Endpoint`. A fast-path
+optimization only re-encoded query parameter values when the parameter keys
were *not* already in
+alphabetical order, so two semantically identical URIs could normalize to two
different strings whenever a
+value needed encoding (for example a colon in a `host:port` value) - depending
purely on whether the
+original, incidental parameter order happened to already be sorted.
`CamelContext.getEndpoint()` would then
+silently create a duplicate `Endpoint` (and duplicate producers/consumers,
connections, threads) instead of
+reusing the cached one, with no error or log warning.
+
+Normalization is now always order-independent. As part of the fix, the
encoding applied when rebuilding the
+query string is also less aggressive: characters that are legal unescaped in a
URI query per RFC 3986
+(`:`, `/`, `,`, `'`, etc. - for example a MIME type such as
`produces=application/json`, or a `host:port`
+value) are no longer percent-encoded, while `&` and `=` remain escaped inside
a value since they are
+structurally significant in Camel's own `key=value&key=value` query syntax.
+
+Code that asserts a literal, fully-normalized endpoint URI string containing
one of those characters in a
+query value may need to update the expected string to the (now consistently)
unencoded form.
+
+=== camel-core - property placeholders in pollEnrich
+
+Camel 4.22 stopped resolving property placeholders (`{{...}}`) on the
_per-message evaluated_
+recipient for `toD` and `enrich`, and said that aligning `pollEnrich` was
deferred to a follow-up.
+This is that follow-up: a `{{...}}` token that appears only in the value
produced at runtime by the
+`pollEnrich` expression is now treated as a literal part of the endpoint URI
instead of being
+expanded.
+
+Like `toD` and `enrich`, `pollEnrich` resolves its static endpoint URI at
build time, so a
+placeholder belongs there:
+
+[source,java]
+----
+.pollEnrich("file:{{inbox}}", 5000)
+----
+
+`recipientList`, `routingSlip` and `dynamicRouter` are unchanged. Their
recipient is supplied
+entirely at runtime and may legitimately carry a placeholder that comes from
configuration, so they
+continue to resolve `{{...}}` in the computed recipient.
+
+=== camel-core - XmlConverter SAX parser factory
+
+`XmlConverter.createSAXParserFactory()` now also disables external parameter
entities and external
+DTD loading:
+
+* `http://xml.org/sax/features/external-parameter-entities` = `false`
+* `http://apache.org/xml/features/nonvalidating/load-external-dtd` = `false`
+
+It previously set only `FEATURE_SECURE_PROCESSING` and
`external-general-entities=false`, while
+`createDocumentBuilderFactory()` in the same class already blocked external
resource resolution more
+thoroughly. Both factories are reachable from a converted message body —
`toSAXSource` is a
+registered converter, and the SAXSource route is tried first for bodies
reaching camel-xslt — so the
+two should not disagree.
+
+Documents carrying an internal DTD subset still parse: `disallow-doctype-decl`
is deliberately not
+set here, because that would reject input that parses today. Routes that
genuinely need to resolve
+an external DTD or parameter entity through this converter must supply their
own
+`SAXParserFactory`.
+
=== camel-a2a - webhook URL address classification
Push notification webhook URLs are now classified by the address the host
resolves to, using the
@@ -233,25 +292,6 @@ The `CamelAzureEventGridDataVersion` header
(`EventGridConstants.DATA_VERSION`)
component publishes events in the CloudEvents schema, which has no
`dataVersion` attribute (that field
belongs to the legacy Event Grid event schema), so the header was read but
never applied to the
published event. Remove any use of that header; there is no CloudEvents
equivalent.
-=== camel-core - property placeholders in pollEnrich
-
-Camel 4.22 stopped resolving property placeholders (`{{...}}`) on the
_per-message evaluated_
-recipient for `toD` and `enrich`, and said that aligning `pollEnrich` was
deferred to a follow-up.
-This is that follow-up: a `{{...}}` token that appears only in the value
produced at runtime by the
-`pollEnrich` expression is now treated as a literal part of the endpoint URI
instead of being
-expanded.
-
-Like `toD` and `enrich`, `pollEnrich` resolves its static endpoint URI at
build time, so a
-placeholder belongs there:
-
-[source,java]
-----
-.pollEnrich("file:{{inbox}}", 5000)
-----
-
-`recipientList`, `routingSlip` and `dynamicRouter` are unchanged. Their
recipient is supplied
-entirely at runtime and may legitimately carry a placeholder that comes from
configuration, so they
-continue to resolve `{{...}}` in the computed recipient.
=== camel-hazelcast
@@ -360,25 +400,6 @@ Routes that relied on steps after these processors running
for unauthenticated r
restructured. The authenticated paths are unchanged: a successfully
authenticated request continues
through the rest of the route exactly as before, and `OAuthLogoutProcessor` is
unchanged.
-=== camel-core - XmlConverter SAX parser factory
-
-`XmlConverter.createSAXParserFactory()` now also disables external parameter
entities and external
-DTD loading:
-
-* `http://xml.org/sax/features/external-parameter-entities` = `false`
-* `http://apache.org/xml/features/nonvalidating/load-external-dtd` = `false`
-
-It previously set only `FEATURE_SECURE_PROCESSING` and
`external-general-entities=false`, while
-`createDocumentBuilderFactory()` in the same class already blocked external
resource resolution more
-thoroughly. Both factories are reachable from a converted message body —
`toSAXSource` is a
-registered converter, and the SAXSource route is tried first for bodies
reaching camel-xslt — so the
-two should not disagree.
-
-Documents carrying an internal DTD subset still parse: `disallow-doctype-decl`
is deliberately not
-set here, because that would reject input that parses today. Routes that
genuinely need to resolve
-an external DTD or parameter entity through this converter must supply their
own
-`SAXParserFactory`.
-
=== camel-netty-http
The security-constraint lookup now strips the endpoint context-path from the
request target