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

davsclaus pushed a commit to branch CAMEL-25050-route-header-variables
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 9491837d7f37423a6a4f1b032bb13b5946a9a39a
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 1 20:04:58 2026 +0100

    CAMEL-25050: camel-core - variableReceive into a route or group variable 
stores the header variables in that route or group
    
    The header variables of route variable resp in route rs were stored as 
header:rs:resp.status, which
    the route repository split at the first colon into a route with the id 
header. They could only be
    read with route:header:rs:resp.status and were not removed with the route; 
the same for groups.
    
    They are now stored as rs:header:resp.status in the route (group) of the 
variable, so
    route:header:resp.status resolves to the current route, 
route:rs:header:resp.status and
    group:myGroup:header:resp.status work, and removing a route removes them 
too.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../processor/ToVariableReceiveHeadersTest.java    | 56 +++++++++++++++++-----
 .../org/apache/camel/support/ExchangeHelper.java   | 27 ++++++++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 16 +++++++
 docs/user-manual/modules/ROOT/pages/variables.adoc | 13 +++++
 4 files changed, 93 insertions(+), 19 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
index d7cedd34a576..3c0b90da46bb 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
@@ -16,11 +16,14 @@
  */
 package org.apache.camel.processor;
 
+import java.util.Set;
+
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.spi.BrowsableVariableRepository;
 import org.apache.camel.spi.VariableRepositoryFactory;
+import org.apache.camel.support.GroupVariableRepository;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,14 +73,30 @@ class ToVariableReceiveHeadersTest extends 
ContextTestSupport {
         template.sendBody("direct:route", "Hello");
 
         assertMockEndpointsSatisfied();
-        // the header variables of route variable resp in route rs are stored 
with the key header:rs:resp.<header>
+        // the header variables of route variable resp are stored in route rs, 
as header:resp.<header>
         assertEquals("Bye second", context.getVariable("route:rs:resp"));
-        assertEquals(200, context.getVariable("route:header:rs:resp.status"));
-        assertNull(context.getVariable("route:header:rs:resp.error"));
+        assertEquals(200, context.getVariable("route:rs:header:resp.status"));
+        assertNull(context.getVariable("route:rs:header:resp.error"));
         // the header variables of another variable of the route, and of the 
same variable in another route, are kept
-        assertEquals("E42", 
context.getVariable("route:header:rs:other.error"));
-        assertEquals("E42", 
context.getVariable("route:header:rs2:resp.error"));
+        assertEquals("E42", 
context.getVariable("route:rs:header:other.error"));
+        assertEquals("E42", 
context.getVariable("route:rs2:header:resp.error"));
         assertEquals("Bye first", context.getVariable("route:rs2:resp"));
+        // no route with the id header
+        assertNull(context.getVariable("route:header:rs:resp.status"));
+        assertNoRouteOrGroupNamedHeader("route");
+    }
+
+    @Test
+    void testRemoveRouteRemovesHeaderVariables() throws Exception {
+        template.sendBody("direct:route2", "Hello");
+        assertEquals(500, context.getVariable("route:rs2:header:resp.status"));
+
+        context.getRouteController().stopRoute("rs2");
+        context.removeRoute("rs2");
+
+        assertNull(context.getVariable("route:rs2:resp"));
+        assertNull(context.getVariable("route:rs2:header:resp.status"));
+        assertNull(context.getVariable("route:rs2:header:resp.error"));
     }
 
     @Test
@@ -87,14 +106,27 @@ class ToVariableReceiveHeadersTest extends 
ContextTestSupport {
         template.sendBody("direct:group", "Hello");
 
         assertMockEndpointsSatisfied();
-        // the header variables of group variable myGroup:resp are stored with 
the key header:myGroup:resp.<header>
+        // the header variables of group variable myGroup:resp are stored in 
group myGroup, as header:resp.<header>
         assertEquals("Bye second", context.getVariable("group:myGroup:resp"));
-        assertEquals(200, 
context.getVariable("group:header:myGroup:resp.status"));
-        assertNull(context.getVariable("group:header:myGroup:resp.error"));
+        assertEquals(200, 
context.getVariable("group:myGroup:header:resp.status"));
+        assertNull(context.getVariable("group:myGroup:header:resp.error"));
         // the header variables of another variable of the group, and of the 
same variable in another group, are kept
-        assertEquals("E42", 
context.getVariable("group:header:myGroup:other.error"));
-        assertEquals("E42", 
context.getVariable("group:header:otherGroup:resp.error"));
+        assertEquals("E42", 
context.getVariable("group:myGroup:header:other.error"));
+        assertEquals("E42", 
context.getVariable("group:otherGroup:header:resp.error"));
         assertEquals("Bye first", 
context.getVariable("group:otherGroup:resp"));
+        // no group with the id header
+        assertNull(context.getVariable("group:header:myGroup:resp.status"));
+        assertNoRouteOrGroupNamedHeader("group");
+        GroupVariableRepository repo = (GroupVariableRepository) 
context.getCamelContextExtension()
+                
.getContextPlugin(VariableRepositoryFactory.class).getVariableRepository("group");
+        assertEquals(Set.of("myGroup", "otherGroup"), repo.getGroupIds());
+    }
+
+    private void assertNoRouteOrGroupNamedHeader(String id) {
+        BrowsableVariableRepository repo = (BrowsableVariableRepository) 
context.getCamelContextExtension()
+                
.getContextPlugin(VariableRepositoryFactory.class).getVariableRepository(id);
+        assertFalse(repo.getVariables().keySet().stream().anyMatch(k -> 
k.startsWith("header:")),
+                "header variables under the id header in " + id + " 
repository: " + repo.getVariables().keySet());
     }
 
     private void assertNoErrorHeader(String id) {
@@ -126,7 +158,7 @@ class ToVariableReceiveHeadersTest extends 
ContextTestSupport {
                         .setBody(constant("first")).toV("direct:svc", null, 
"route:resp")
                         .setBody(constant("second")).toV("direct:svc", null, 
"route:resp")
                         .setBody(simple(
-                                
"${variable.route:resp}|${variable.route:header:rs:resp.status}|${variable.route:header:rs:resp.error}"))
+                                
"${variable.route:resp}|${variable.route:header:resp.status}|${variable.route:header:resp.error}"))
                         .to("mock:route");
 
                 from("direct:route2").routeId("rs2")
@@ -138,7 +170,7 @@ class ToVariableReceiveHeadersTest extends 
ContextTestSupport {
                         .setBody(constant("first")).toV("direct:svc", null, 
"group:myGroup:resp")
                         .setBody(constant("second")).toV("direct:svc", null, 
"group:myGroup:resp")
                         .setBody(simple(
-                                
"${variable.group:myGroup:resp}|${variable.group:header:myGroup:resp.status}|${variable.group:header:myGroup:resp.error}"))
+                                
"${variable.group:myGroup:resp}|${variable.group:myGroup:header:resp.status}|${variable.group:myGroup:header:resp.error}"))
                         .to("mock:group");
 
                 // only the reply to the first request has the error header
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
index bd23381a783e..e8db04bf1570 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
@@ -1234,7 +1234,8 @@ public final class ExchangeHelper {
     public static String resolveVariableRepositoryName(Exchange exchange, 
String name, String id) {
         name = StringHelper.after(name, ":");
         // special for route, where we need to enrich the name with current 
route id if none given
-        if ("route".equals(id) && !name.contains(":")) {
+        // (also for the header variables of a route variable, such as 
header:resp.status)
+        if ("route".equals(id) && !hasRouteId(name)) {
             String prefix = getAtRouteId(exchange);
             if (prefix != null) {
                 name = prefix + ":" + name;
@@ -1246,6 +1247,13 @@ public final class ExchangeHelper {
         return name;
     }
 
+    private static boolean hasRouteId(String name) {
+        if (name.startsWith("header:")) {
+            name = name.substring(7);
+        }
+        return name.contains(":");
+    }
+
     /**
      * Sets the variable from the given message body and headers
      *
@@ -1264,9 +1272,15 @@ public final class ExchangeHelper {
         final VariableAware va = getVariableAware(exchange, repo);
 
         // the headers are stored as header:name.key (in a route or group 
repository the name is id:name, so the key
-        // is header:id:name.key). Remove the headers of a previous message 
stored in this variable, so a header that
-        // this message does not have is not kept with its old value
-        String prefix = "header:" + name + ".";
+        // is id:header:name.key, which keeps the headers in the route or 
group of the variable). Remove the headers of
+        // a previous message stored in this variable, so a header that this 
message does not have is not kept with
+        // its old value
+        String prefix;
+        if (repo instanceof RouteVariableRepository || repo instanceof 
GroupVariableRepository) {
+            prefix = StringHelper.before(name, ":") + ":header:" + 
StringHelper.after(name, ":") + ".";
+        } else {
+            prefix = "header:" + name + ".";
+        }
         removeVariables(exchange, repo, prefix);
 
         // set body and headers as variables
@@ -1280,9 +1294,8 @@ public final class ExchangeHelper {
     }
 
     private static void removeVariables(Exchange exchange, VariableRepository 
repo, String prefix) {
-        // the route and group repositories only scan the map of the id parsed 
from the prefix instead of copying
-        // the whole repository. For a prefix like header:<routeId>:<var>. 
that id is always header (CAMEL-25050),
-        // so the scan covers the shared header map of all routes or groups
+        // the route and group repositories only scan the map of the route or 
group of the variable instead of copying
+        // the whole repository
         if (repo instanceof RouteVariableRepository route) {
             route.removeVariablesWithPrefix(prefix);
             return;
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index b50ad720d318..04124a61bef8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -319,6 +319,22 @@ caller), put the EIP in a `doTry` with only a `doFinally` 
in the called route, o
 called route with `errorHandler(noErrorHandler())`, which turns it off for the 
other steps of that route too. Neither
 option brings back the redelivery of a whole routing slip or dynamic router.
 
+=== camel-core - variableReceive into a route or group variable stores the 
header variables in that route or group
+
+With `variableReceive` (`toV`, `enrich`, `pollEnrich`, `fromV` and so on) into 
a route variable or a group variable,
+the received headers are now stored in the route (group) of the variable. 
Before, they were stored under a route
+(group) with the id `header`, so they could only be read with an internal 
name, and were not removed with the route.
+
+[width="100%",cols="1m,2m,2m",options="header"]
+|===
+| Variable | Header variable before | Header variable now
+| route:resp (in route rs) | route:header:rs:resp.status | 
route:header:resp.status or route:rs:header:resp.status
+| group:myGroup:resp | group:header:myGroup:resp.status | 
group:myGroup:header:resp.status
+|===
+
+Inside a route, `route:header:resp.status` now means the header variable of 
the route variable `resp` of the current
+route, not the variable `resp.status` of a route with the id `header`.
+
 === camel-util - FilterIterator.remove after hasNext throws 
IllegalStateException
 
 `FilterIterator.hasNext()` looks ahead in the iterator it wraps, so a 
`remove()` after `next()` and then `hasNext()`
diff --git a/docs/user-manual/modules/ROOT/pages/variables.adoc 
b/docs/user-manual/modules/ROOT/pages/variables.adoc
index b197f16eff80..0f5442146319 100644
--- a/docs/user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/user-manual/modules/ROOT/pages/variables.adoc
@@ -379,6 +379,19 @@ The *VariableReceive* works in a different mode. The idea 
is that all the receiv
 is not changed at all. The received body is stored in the variable, and the 
received headers (transport headers etc.) are stored as read-only
 headers as variables as well. The names of the variable is 
`header:variableName.headerName`. For example, if the variable is `myVar` and 
the header is `Content-Type`
 then the header is stored as a variable with the full name 
`header:myVar.Content-Type`.
+For a global variable the `header:` goes after the repository: `global:myVar` 
has the header variable `global:header:myVar.Content-Type`.
+A route or group variable keeps its header variables in the same route or 
group as the variable:
+
+[width="100%",cols="1m,2m",options="header"]
+|===
+| Variable | Header variable
+| route:myVar | route:header:myVar.Content-Type (in the current route)
+| route:myRoute:myVar | route:myRoute:header:myVar.Content-Type
+| group:myGroup:myVar | group:myGroup:header:myVar.Content-Type
+|===
+
+When a route is removed, its route variables are removed together with their 
header variables.
+
 When data is received into the same variable again, the header variables from 
the previous time are removed first,
 so the header variables are always the headers of the last received message. 
This is done for exchange, global, route
 and group variables. A custom variable repository that cannot be browsed keeps 
the old header variables.

Reply via email to