This is an automated email from the ASF dual-hosted git repository.

davsclaus 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 e117ccc77ccd CAMEL-24747: Preserve raw URI for useRawUri() components 
when using toD
e117ccc77ccd is described below

commit e117ccc77ccdbb07f030f37ae2353ed9afda0c12
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Sep 17 07:49:46 2026 +0200

    CAMEL-24747: Preserve raw URI for useRawUri() components when using toD
    
    When SendDynamicProcessor (toD) resolves a dynamic endpoint it wraps the
    URI in a NormalizedUri, which only carries the URL-encoded form. That
    encoded string was then handed to component.createEndpoint() even for
    components that declare useRawUri()=true, so kamelet parameters such as
    http://example.com?key=abc%+def arrived double-encoded. The static to()
    DSL was unaffected because it captures the raw URI before normalization.
    
    NormalizedUri now keeps the original un-normalized URI alongside the
    normalized cache key, and DefaultCamelContextExtension passes it to a new
    AbstractCamelContext.doGetEndpoint overload so useRawUri() components
    receive the same raw string from toD as from to. Adds a regression test
    for camel-kamelet and an upgrade-guide note for 4.23.
    
    Closes #26456
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../kamelet/KameletToDUrlEncodingTest.java         | 109 +++++++++++++++++++++
 .../camel/impl/engine/AbstractCamelContext.java    |   7 +-
 .../impl/engine/DefaultCamelContextExtension.java  |   9 +-
 .../org/apache/camel/support/NormalizedUri.java    |  29 +++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  14 +++
 5 files changed, 160 insertions(+), 8 deletions(-)

diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletToDUrlEncodingTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletToDUrlEncodingTest.java
new file mode 100644
index 000000000000..b26907f6cf85
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletToDUrlEncodingTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.kamelet;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that kamelet parameters containing URL-special characters are 
preserved intact when the kamelet is invoked
+ * via {@code toD}, matching the behaviour of the static {@code to} DSL 
(CAMEL-24747).
+ *
+ * <p>
+ * Prior to the fix, {@code SendDynamicProcessor.prepareRecipient()} created a
+ * {@link org.apache.camel.support.NormalizedUri} whose normalized 
(URL-encoded) form was then passed to
+ * {@code doGetEndpoint} as if it were the raw URI. Components that declare 
{@code useRawUri()=true} (such as
+ * {@link KameletComponent}) therefore received the encoded form ({@code 
http%3A%2F%2F…}) instead of the original value.
+ */
+class KameletToDUrlEncodingTest extends CamelTestSupport {
+
+    /**
+     * A parameter value that contains URL-special characters: {@code %}, 
{@code +}, {@code ?} and {@code =}. Used by
+     * the static {@code to} route as a baseline — known to pass through 
unmodified.
+     */
+    private static final String PARAM_WITH_SPECIAL_CHARS = 
"http://example.com?key=abc%+def";;
+
+    /**
+     * A distinct parameter value (different from {@link 
#PARAM_WITH_SPECIAL_CHARS}) used exclusively for the
+     * {@code toD} route. Using a different value prevents the {@code toD} 
route from hitting the endpoint-cache entry
+     * registered at startup by the static {@code to} route, ensuring that 
{@code doGetEndpoint} is actually exercised
+     * for the dynamic path (CAMEL-24747).
+     */
+    private static final String PARAM_WITH_SPECIAL_CHARS_TOD = 
"http://example.com?key=xyz%+def";;
+
+    @Test
+    void toDPreservesSpecialCharsLikeTo() {
+        // Baseline: static `to` route with URL-special characters in a 
parameter value.
+        String resultTo = template.requestBody("direct:via-to", (Object) null, 
String.class);
+        assertThat(resultTo)
+                .as("to: parameter value must not be URL-encoded")
+                .isEqualTo(PARAM_WITH_SPECIAL_CHARS);
+
+        // Dynamic `toD` route uses a distinct value so the endpoint-cache 
populated by
+        // the `to` route above cannot mask a regression (CAMEL-24747).
+        String resultToD = template.requestBody("direct:via-tod", (Object) 
null, String.class);
+        assertThat(resultToD)
+                .as("toD: parameter value must not be URL-encoded 
(CAMEL-24747)")
+                .isEqualTo(PARAM_WITH_SPECIAL_CHARS_TOD);
+    }
+
+    /**
+     * Negative test: components that do NOT declare {@code useRawUri()=true} 
continue to receive the normalised URI via
+     * {@code toD}, proving that the fix is surgically scoped to {@code 
useRawUri} components only.
+     */
+    @Test
+    void toDNonRawUriComponentIsUnaffected() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+        mock.expectedBodiesReceived("hello");
+
+        template.sendBody("direct:via-tod-mock", "hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("echo-uri")
+                        .templateParameter("uri")
+                        .from("kamelet:source")
+                        .setBody().constant("{{uri}}");
+
+                // static `to` — baseline: known to pass the raw URI correctly
+                from("direct:via-to")
+                        .to("kamelet:echo-uri?uri=" + 
PARAM_WITH_SPECIAL_CHARS);
+
+                // dynamic `toD` — was broken before the fix (CAMEL-24747); 
uses a distinct
+                // value to bypass the startup-time endpoint-cache entry of 
the `to` route
+                from("direct:via-tod")
+                        .toD("kamelet:echo-uri?uri=" + 
PARAM_WITH_SPECIAL_CHARS_TOD);
+
+                // non-useRawUri component via toD — must continue to work 
normally
+                from("direct:via-tod-mock")
+                        .toD("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 0d4403dcaf6b..2f7ec6a11b1e 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -788,6 +788,11 @@ public abstract class AbstractCamelContext extends 
BaseService
     }
 
     protected Endpoint doGetEndpoint(String uri, Map<String, Object> 
parameters, boolean normalized, boolean prototype) {
+        return doGetEndpoint(uri, null, parameters, normalized, prototype);
+    }
+
+    protected Endpoint doGetEndpoint(
+            String uri, String explicitRawUri, Map<String, Object> parameters, 
boolean normalized, boolean prototype) {
         // ensure CamelContext are initialized before we can get an endpoint
         build();
 
@@ -802,7 +807,7 @@ public abstract class AbstractCamelContext extends 
BaseService
             uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(this, 
uri);
         }
 
-        final String rawUri = uri;
+        final String rawUri = explicitRawUri != null ? explicitRawUri : uri;
 
         // normalize uri so we can do endpoint hits with minor mistakes and
         // parameters is not in the same order
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
index 9bfa5d15d3f6..d2cce18c9a82 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
@@ -269,7 +269,8 @@ class DefaultCamelContextExtension implements 
ExtendedCamelContext {
 
     @Override
     public Endpoint getEndpoint(NormalizedEndpointUri uri) {
-        return camelContext.doGetEndpoint(uri.getUri(), null, true, false);
+        String rawUri = uri instanceof NormalizedUri nu ? nu.getRawUri() : 
null;
+        return camelContext.doGetEndpoint(uri.getUri(), rawUri, null, true, 
false);
     }
 
     @Override
@@ -279,12 +280,14 @@ class DefaultCamelContextExtension implements 
ExtendedCamelContext {
 
     @Override
     public Endpoint getPrototypeEndpoint(NormalizedEndpointUri uri) {
-        return camelContext.doGetEndpoint(uri.getUri(), null, true, true);
+        String rawUri = uri instanceof NormalizedUri nu ? nu.getRawUri() : 
null;
+        return camelContext.doGetEndpoint(uri.getUri(), rawUri, null, true, 
true);
     }
 
     @Override
     public Endpoint getEndpoint(NormalizedEndpointUri uri, Map<String, Object> 
parameters) {
-        return camelContext.doGetEndpoint(uri.getUri(), parameters, true, 
false);
+        String rawUri = uri instanceof NormalizedUri nu ? nu.getRawUri() : 
null;
+        return camelContext.doGetEndpoint(uri.getUri(), rawUri, parameters, 
true, false);
     }
 
     @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/NormalizedUri.java 
b/core/camel-support/src/main/java/org/apache/camel/support/NormalizedUri.java
index 690820b99bbb..cbd9f543674d 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/NormalizedUri.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/NormalizedUri.java
@@ -28,8 +28,14 @@ public final class NormalizedUri extends ValueHolder<String> 
implements Normaliz
 
     // must extend ValueHolder to let this class be used as key for Camels 
endpoint registry
 
-    private NormalizedUri(String value) {
-        super(value);
+    // The raw (un-normalized) URI, preserved so that components with 
useRawUri()=true
+    // receive the original unencoded form even when the endpoint is resolved 
via a
+    // NormalizedEndpointUri (e.g. from SendDynamicProcessor / toD).
+    private final String rawUri;
+
+    private NormalizedUri(String normalizedValue, String rawUri) {
+        super(normalizedValue);
+        this.rawUri = rawUri;
     }
 
     /**
@@ -41,12 +47,27 @@ public final class NormalizedUri extends 
ValueHolder<String> implements Normaliz
      */
     public static NormalizedUri newNormalizedUri(String uri, boolean 
normalized) {
         if (normalized) {
-            return new NormalizedUri(uri);
+            return new NormalizedUri(uri, null);
         } else {
-            return new NormalizedUri(EndpointHelper.normalizeEndpointUri(uri));
+            return new NormalizedUri(EndpointHelper.normalizeEndpointUri(uri), 
uri);
         }
     }
 
+    /**
+     * Returns the raw (un-normalized) URI that was used to create this 
instance. Components that declare
+     * {@code useRawUri()=true} should receive this value so that parameter 
values are not URL-decoded before they reach
+     * the component.
+     *
+     * <p>
+     * May be {@code null} when the raw form is not known (e.g. when the URI 
was already normalized at construction
+     * time). Callers should treat {@code null} as "fall back to the 
normalized URI".
+     *
+     * @since 4.23
+     */
+    public String getRawUri() {
+        return rawUri;
+    }
+
     @Override
     public String getUri() {
         return get();
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 819de4a90f74..79340c447653 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
@@ -2178,3 +2178,17 @@ It was read by the Weaviate embeddings data-type 
transformer but its value was n
 vector is always sent as the object vector, so a "vector field name" had no 
effect (Weaviate has no named
 vector-field concept in this producer). Routes that set this header can simply 
drop it; behaviour is
 unchanged.
+
+=== toD now passes the raw URI to useRawUri() components
+
+Components that override `useRawUri()` to return `true` (such as `kamelet`, 
`master`, and `webhook`) now
+receive the original, un-encoded URI when the endpoint is resolved dynamically 
via `toD`, exactly as they
+already did via the static `to` DSL.
+
+Before this change, `toD` passed the URL-encoded (normalised) form to such 
components, causing parameter
+values containing special characters (`%`, `+`, `?`, `=`, `@`, `|`) to arrive 
double-encoded at the
+component.
+
+This is a bug fix (CAMEL-24747). Routes that relied on `toD` passing the 
encoded form to a `useRawUri()`
+component will now see the original raw value instead. For all other 
components (`useRawUri()` returns
+`false`), behaviour is unchanged.

Reply via email to