From f0964fc672fbe70b7d16fb9373b6d378f8d71186 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Wed, 30 Sep 2026 15:32:47 +0200 Subject: [PATCH 1/2] CAMEL-25188: camel-util - URISupport.normalizeUri writes a space in a query value as + in the fast path too Since CAMEL-24524 the fast path wrote a space as %20 while the complex path writes +, so normalizing a normalized uri changed it, and the same endpoint written as a+b or a%20b got two endpoint keys. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../org/apache/camel/util/URISupport.java | 3 ++ .../org/apache/camel/util/URISupportTest.java | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) 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 25250ff857b6b..0f97643e11e6f 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 @@ -955,6 +955,9 @@ private static void appendSafeQueryStringParameter(String key, String value, Str // 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"); + // a space as +, as the complex normalizer (createQueryString) writes it, so normalizing a normalized + // uri gives the same uri; the fast parser only takes uris without %, so %20 here is always a space + encoded = encoded.replace("%20", "+"); sb.append(encoded); } } 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 4cd22a9b2bf01..a6e32cc42fe3a 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 @@ -148,6 +148,40 @@ public void testNormalizeHttpEndpointURLEncodedParameter() throws Exception { assertEquals("http://www.google.com?q=S%C3%B8ren+Hansen", out); } + @Test + public void testNormalizeSpaceTheSameInEverySpelling() throws Exception { + // CAMEL-25188: the fast and the complex normalizer both write a space in a value as + + assertThat(URISupport.normalizeUri("log:foo?marker=a+b")).isEqualTo("log://foo?marker=a+b"); + assertThat(URISupport.normalizeUri("log:foo?marker=a%20b")).isEqualTo("log://foo?marker=a+b"); + assertThat(URISupport.normalizeUri("log:foo?marker=a b")).isEqualTo("log://foo?marker=a+b"); + assertThat(URISupport.normalizeUri("log:foo?showAll=true&marker=a+b")) + .isEqualTo("log://foo?marker=a+b&showAll=true"); + assertThat(URISupport.normalizeUri("log:foo?marker=a++b")).isEqualTo("log://foo?marker=a++b"); + } + + @Test + public void testNormalizeTwiceGivesTheSameUri() throws Exception { + // CAMEL-25188: a normalized uri normalizes to itself, so endpoint keys and lookups agree + String[] uris = { + "http://localhost:8080/foo?a=1&b=2", + "http://localhost:8080/foo?b=hello world&a=1", + "http://localhost:8080/foo?q=a+b", + "http://localhost:8080/foo?q=a%20b", + "http://localhost:8080/foo?q=a+b&x=1", + "ftp://user@host.com:21/dir?password=se+cret&binary=true", + "ftp://user@host.com:21/dir?password=RAW(se+cret)&binary=true", + "log:foo?level=INFO&showAll=true", + "timer:tick?period=1s&delay=2s", + "direct:start?b=\u00f8&a=1", + "file:target/in?include=.*\\.txt&noop=true", + "http://h/p?x=a&x=b&y=1", + "mock:a?b=x+y+z&a=1" }; + for (String uri : uris) { + String once = URISupport.normalizeUri(uri); + assertThat(URISupport.normalizeUri(once)).as("normalizing %s twice", uri).isEqualTo(once); + } + } + @Test public void testParseParametersURLEncodedValue() throws Exception { String out = URISupport.normalizeUri("http://www.google.com?q=S%C3%B8ren%20Hansen"); From 9a2e5da551bd96907099ee366f4c3fa97ca27a56 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Thu, 1 Oct 2026 15:32:22 +0200 Subject: [PATCH 2/2] CAMEL-25188: camel-util - normalizeUri form-encodes the query when a key or value needs a percent escape A value with = or # got a percent escape in the fast path, and a uri with % goes to the complex path, which form-encodes the whole query, so normalizing a normalized uri changed it again (eg selector=somekey='somevalue' or secretKey=abc/def==) and the same endpoint could get two keys. The fast path now form-encodes the query the same way in that case, and encodes keys as values, so a + in a key is kept. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../org/apache/camel/util/URISupport.java | 29 +++++++++------ .../org/apache/camel/util/URISupportTest.java | 35 ++++++++++++++++--- .../pages/camel-4x-upgrade-guide-4_23.adoc | 6 ++-- 3 files changed, 54 insertions(+), 16 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 0f97643e11e6f..004425829f160 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 @@ -900,6 +900,12 @@ private static String buildReorderingParameters(String scheme, String path, Stri // 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); + if (query.indexOf('%') != -1) { + // a key or value needed a percent escape (such as = or # in a value), and a uri with % is normalized by + // the complex normalizer, which form-encodes the whole query; encode the same way here so normalizing a + // normalized uri gives the same uri (the fast parser only takes uris without %, so all % come from here) + query = createQueryString(keys, parameters, true); + } return buildUri(scheme, path, query); } @@ -939,7 +945,7 @@ private static String buildSafeQueryString(String[] sortedKeys, Map