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

oscerd 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 655ef46c141d CAMEL-24282: Resolve dynamic-URI property placeholders at 
build time instead of per message (#25198)
655ef46c141d is described below

commit 655ef46c141df9a7029467756c307b808aeee17e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jul 30 10:10:40 2026 +0200

    CAMEL-24282: Resolve dynamic-URI property placeholders at build time 
instead of per message (#25198)
    
    Resolve property placeholders ({{...}}) once at build time for the 
dynamic-URI EIPs toD and enrich, instead of re-expanding them per message on 
the evaluated recipient. A {{...}} token that appears only in the per-message 
recipient (e.g. supplied via a header) is now treated as a literal endpoint 
uri, matching the documented build-time resolution contract. 
recipientList/routingSlip/dynamicRouter/pollEnrich are unchanged.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../camel/processor/SendDynamicProcessor.java      |  17 ++-
 .../DynamicEndpointMessagePlaceholderTest.java     | 154 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  32 +++++
 3 files changed, 194 insertions(+), 9 deletions(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/SendDynamicProcessor.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/SendDynamicProcessor.java
index 543c8f04c730..2104806dff08 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/SendDynamicProcessor.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/SendDynamicProcessor.java
@@ -42,6 +42,7 @@ import org.apache.camel.spi.SendDynamicAware;
 import org.apache.camel.spi.StepIdAware;
 import org.apache.camel.support.EndpointHelper;
 import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.NormalizedUri;
 import org.apache.camel.support.cache.DefaultProducerCache;
 import org.apache.camel.support.cache.EmptyProducerCache;
 import org.apache.camel.support.service.ServiceHelper;
@@ -278,13 +279,10 @@ public class SendDynamicProcessor extends 
BaseProcessorSupport
             uri = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(String.class, 
exchange, recipient);
         }
 
-        // in case path has property placeholders then try to let property 
component resolve those
-        try {
-            uri = 
EndpointHelper.resolveEndpointUriPropertyPlaceholders(exchange.getContext(), 
uri);
-        } catch (Exception e) {
-            throw new ResolveEndpointFailedException(uri, e);
-        }
-
+        // NOTE: property placeholders in the dynamic-uri template are 
resolved once at build time
+        // (see ToDynamicReifier#createExpression). The per-message evaluated 
recipient must not be
+        // re-resolved here: message content that happens to contain {{...}} 
must be treated as a
+        // literal endpoint uri, not expanded as a property placeholder 
(CAMEL-24282).
         return uri;
     }
 
@@ -313,8 +311,9 @@ public class SendDynamicProcessor extends 
BaseProcessorSupport
             if (colon == -1 || colon == uri.length() - 1) {
                 throw new ResolveEndpointFailedException(uri, "Endpoint should 
include scheme:path");
             }
-            // optimize and normalize endpoint
-            return ecc.getCamelContextExtension().normalizeUri(uri);
+            // optimize and normalize endpoint without re-resolving property 
placeholders on the
+            // per-message evaluated recipient (see resolveUri and CAMEL-24282)
+            return NormalizedUri.newNormalizedUri(uri, false);
         }
         return null;
     }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/DynamicEndpointMessagePlaceholderTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/DynamicEndpointMessagePlaceholderTest.java
new file mode 100644
index 000000000000..573d6a6afd5a
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/DynamicEndpointMessagePlaceholderTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.processor;
+
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Header;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.seda.SedaEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The dynamic-uri EIPs {@code toD} and {@code enrich} resolve property 
placeholders ({@code {{...}}}) once, at build
+ * time, on the endpoint uri written in the route (via 
ToDynamicReifier/EnrichReifier). A {@code {{...}}} token that
+ * only appears in the per-message evaluated recipient (e.g. from a header) is 
therefore treated as a literal endpoint
+ * uri and not re-expanded. See CAMEL-24282.
+ * <p/>
+ * {@code recipientList} / {@code routingSlip} / {@code dynamicRouter} compute 
their recipients entirely from a runtime
+ * expression (no static template resolved at build time), so they continue to 
resolve {@code {{...}}} in those
+ * recipients - this is relied upon e.g. by routes carrying a {@code 
{{port}}}-style placeholder in the recipient
+ * header, and must be preserved. {@code pollEnrich} does resolve its static 
uri at build time, but its per-message
+ * recipient goes through the same shared resolution path, so it too still 
resolves {@code {{...}}} (left unchanged in
+ * this PR).
+ */
+class DynamicEndpointMessagePlaceholderTest extends ContextTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        Properties prop = new Properties();
+        prop.setProperty("secretTarget", "resolved");
+        context.getPropertiesComponent().setInitialProperties(prop);
+        return context;
+    }
+
+    @Test
+    void toDPlaceholderInHeaderNotResolved() throws Exception {
+        getMockEndpoint("mock:resolved").expectedMessageCount(0);
+        getMockEndpoint("mock:done").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:tod", "Hello", "target", 
"mock:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void enrichPlaceholderInHeaderNotResolved() throws Exception {
+        getMockEndpoint("mock:resolved").expectedMessageCount(0);
+        getMockEndpoint("mock:done").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:en", "Hello", "target", 
"mock:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void recipientListPlaceholderInHeaderStillResolved() throws Exception {
+        // recipientList has no build-time template, so {{...}} in the 
recipient header is still resolved
+        getMockEndpoint("mock:resolved").expectedMessageCount(1);
+        getMockEndpoint("mock:done").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:rl", "Hello", "target", 
"mock:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void routingSlipPlaceholderInHeaderStillResolved() throws Exception {
+        getMockEndpoint("mock:resolved").expectedMessageCount(1);
+        getMockEndpoint("mock:done").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:rs", "Hello", "target", 
"mock:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void dynamicRouterPlaceholderInHeaderStillResolved() throws Exception {
+        // dynamicRouter extends routingSlip, sharing the same runtime 
recipient path
+        getMockEndpoint("mock:resolved").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:dr", "Hello", "target", 
"mock:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void pollEnrichPlaceholderInHeaderStillResolved() throws Exception {
+        // pollEnrich's per-message recipient goes through the shared 
resolution path, so {{...}} is still resolved
+        template.sendBody("seda:resolved", "SEED");
+        getMockEndpoint("mock:done").expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:pe", "trigger", "target", 
"seda:{{secretTarget}}");
+
+        assertMockEndpointsSatisfied();
+        // resolved -> pollEnrich drained the seed from seda:resolved (not 
from the literal seda:{{secretTarget}})
+        SedaEndpoint seda = context.getEndpoint("seda:resolved", 
SedaEndpoint.class);
+        assertThat(seda.getQueue()).isEmpty();
+    }
+
+    @Test
+    void placeholderInStaticTemplateIsResolved() throws Exception {
+        // control: a placeholder written in the route template itself is 
still resolved at build time
+        getMockEndpoint("mock:resolved").expectedMessageCount(1);
+
+        template.sendBody("direct:staticTemplate", "Hello");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    /**
+     * Dynamic router bean: routes to the header-supplied recipient on the 
first hop, then stops.
+     */
+    public String route(@Header("target") String target, 
@Header(Exchange.SLIP_ENDPOINT) String previous) {
+        return previous == null ? target : null;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:tod").toD("${header.target}").to("mock:done");
+                from("direct:en").enrich().simple("${header.target}")
+                        
.aggregationStrategy(AggregationStrategies.useOriginal()).to("mock:done");
+                
from("direct:rl").recipientList(header("target")).to("mock:done");
+                
from("direct:rs").routingSlip(header("target")).to("mock:done");
+                
from("direct:dr").dynamicRouter(method(DynamicEndpointMessagePlaceholderTest.this,
 "route"));
+                
from("direct:pe").pollEnrich().simple("${header.target}").timeout(2000).end().to("mock:done");
+                from("direct:staticTemplate").toD("mock:{{secretTarget}}");
+            }
+        };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index bc48de399f7f..fcd765269257 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -13,6 +13,38 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading Camel 4.21 to 4.22
 
+=== camel-core
+
+==== Property placeholders in toD and enrich dynamic endpoint URIs
+
+`toD` and `enrich` no longer resolve Camel property placeholders (`{{...}}`) 
on the _per-message
+evaluated_ recipient. Property placeholders are resolved once, at build time, 
on the endpoint URI
+written in the route (the static template); a `{{...}}` token that only 
appears at runtime in the
+value produced by the `toD` / `enrich` expression (for example coming from a 
message header or
+body) is now treated as a literal part of the endpoint URI instead of being 
expanded.
+
+This only affects routes that produced a `{{...}}` token from message content 
and relied on it
+being expanded, such as a `toD` whose recipient came from a header that 
contained a `{{...}}`
+placeholder. Placeholders written directly in the route continue to work 
unchanged, for example:
+
+[source,java]
+----
+.toD("{{myEndpoint}}/${header.id}")
+.toD("mock:{{name}}")
+----
+
+`recipientList`, `routingSlip` and `dynamicRouter` are unchanged: their 
recipients are computed
+entirely from a runtime expression (there is no static template resolved at 
build time), so they
+continue to resolve `{{...}}` placeholders in the computed recipient.
+
+`pollEnrich` is also left unchanged in this release. It does resolve the 
static endpoint URI at
+build time (like `toD` / `enrich`), but its per-message recipient goes through 
the same shared
+resolution path as `recipientList` / `routingSlip`, so it still resolves 
`{{...}}` in the computed
+recipient; aligning it with `toD` / `enrich` is deferred to a follow-up.
+
+If you need a placeholder resolved by `toD` / `enrich`, keep it in the route's 
endpoint URI rather
+than in the message.
+
 === camel-jbang
 
 The Camel JBang CLI (Camel CLI) and TUI have been promoted from _Preview_ to 
_Stable_ support level.

Reply via email to