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.