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 34252a555ea1 CAMEL-26853: Keep internal OData request headers out of 
mapped results (#29627)
34252a555ea1 is described below

commit 34252a555ea1841e05a8dccceaf93824505058db
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 14:34:28 2026 +0100

    CAMEL-26853: Keep internal OData request headers out of mapped results 
(#29627)
    
    * CAMEL-26853: Keep internal OData request headers out of mapped results
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-26853: Address review feedback - the headers of the delegated HTTP 
request are not copied to the result either
    
    CamelHttpUri, CamelHttpQuery, CamelHttpMethod and Accept are set for the 
HTTP
    request the producer delegates to and are not part of the OData result; 
copied
    back, a later HTTP step in the route sent its request to the OData target 
with
    the OData method. They are removed from the HTTP exchange before the 
copy-back,
    next to the generated Authorization header, which keeps any value of them 
the
    caller has on the exchange. The upgrade guide entry says so.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-26853: Cover caller result header preservation
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Codex <[email protected]>
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../apache/camel/catalog/docs/odata-component.adoc |   7 ++
 .../camel-odata/src/main/docs/odata-component.adoc |   7 ++
 .../camel/component/odata/ODataProducer.java       |  18 +++-
 .../camel/component/odata/ODataProducerTest.java   | 102 ++++++++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 +++
 5 files changed, 144 insertions(+), 3 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
index 657e5e417193..28a5df8fbe82 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
@@ -199,6 +199,13 @@ A message-level `Authorization` header can also be 
supplied.
 
 When an `Authorization` header is present, it takes precedence over 
authentication configured on the OData endpoint.
 
+Authentication configured on the endpoint is applied only to the delegated 
HTTP request and does not add an
+`Authorization` header to the result. A caller-supplied `Authorization` header 
remains on the exchange.
+
+The `CamelHttpUri`, `CamelHttpQuery`, `CamelHttpMethod` and `Accept` headers 
that the OData producer sets for the
+delegated HTTP request are also excluded from the result. Caller-supplied 
values for these headers remain on the
+exchange.
+
 If the OData route is exposed to an upstream HTTP consumer, such as 
`platform-http`, take care that an inbound `Authorization` header may be 
forwarded to the OData service.
 
 If the upstream authorization credentials must not be forwarded, remove the 
header before sending the exchange to the OData component.
diff --git a/components/camel-odata/src/main/docs/odata-component.adoc 
b/components/camel-odata/src/main/docs/odata-component.adoc
index 657e5e417193..28a5df8fbe82 100644
--- a/components/camel-odata/src/main/docs/odata-component.adoc
+++ b/components/camel-odata/src/main/docs/odata-component.adoc
@@ -199,6 +199,13 @@ A message-level `Authorization` header can also be 
supplied.
 
 When an `Authorization` header is present, it takes precedence over 
authentication configured on the OData endpoint.
 
+Authentication configured on the endpoint is applied only to the delegated 
HTTP request and does not add an
+`Authorization` header to the result. A caller-supplied `Authorization` header 
remains on the exchange.
+
+The `CamelHttpUri`, `CamelHttpQuery`, `CamelHttpMethod` and `Accept` headers 
that the OData producer sets for the
+delegated HTTP request are also excluded from the result. Caller-supplied 
values for these headers remain on the
+exchange.
+
 If the OData route is exposed to an upstream HTTP consumer, such as 
`platform-http`, take care that an inbound `Authorization` header may be 
forwarded to the OData service.
 
 If the upstream authorization credentials must not be forwarded, remove the 
header before sending the exchange to the OData component.
diff --git 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
index 61294bc73b12..9577b559843f 100644
--- 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
+++ 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
@@ -26,6 +26,7 @@ import org.apache.camel.Component;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
 import org.apache.camel.Producer;
 import org.apache.camel.component.http.HttpConstants;
 import org.apache.camel.support.DefaultProducer;
@@ -125,6 +126,7 @@ public class ODataProducer extends DefaultProducer {
         // Manage Authentication Headers Precedence
         String authHeader = exchange.getMessage().getHeader("Authorization", 
String.class);
         ODataConfiguration config = endpoint.getConfiguration();
+        boolean generatedAuthorization = false;
 
         if (authHeader == null && config != null) {
             if ("Basic".equalsIgnoreCase(config.getAuthMethod())
@@ -133,8 +135,10 @@ public class ODataProducer extends DefaultProducer {
                 String credentials = config.getAuthUsername() + ":" + 
config.getAuthPassword();
                 String encoded = 
Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
                 httpExchange.getMessage().setHeader("Authorization", "Basic " 
+ encoded);
+                generatedAuthorization = true;
             } else if (config.getAuthBearerToken() != null) {
                 httpExchange.getMessage().setHeader("Authorization", "Bearer " 
+ config.getAuthBearerToken());
+                generatedAuthorization = true;
             }
         }
 
@@ -180,7 +184,19 @@ public class ODataProducer extends DefaultProducer {
             return;
         }
 
-        MessageHelper.copyHeaders(httpExchange.getMessage(), 
exchange.getMessage(), true);
+        Message result = httpExchange.getMessage();
+        // These headers were set above for the delegated request only and are 
not part of the OData result: copied
+        // back, a later HTTP step in the route would send its request to the 
OData target, with the OData method.
+        // Removing them from the HTTP exchange keeps any value of them the 
caller has on the exchange.
+        result.removeHeader(Exchange.HTTP_URI);
+        result.removeHeader(Exchange.HTTP_QUERY);
+        result.removeHeader(HttpConstants.HTTP_METHOD);
+        result.removeHeader("Accept");
+        if (generatedAuthorization) {
+            // Endpoint authentication belongs to the internal HTTP request, 
not the mapped result.
+            result.removeHeader("Authorization");
+        }
+        MessageHelper.copyHeaders(result, exchange.getMessage(), true);
 
         Integer responseCode = httpExchange.getMessage().getHeader(
                 Exchange.HTTP_RESPONSE_CODE, Integer.class);
diff --git 
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
 
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
index 94102786e804..d90ecc4ab4fe 100644
--- 
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
+++ 
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
@@ -40,6 +40,7 @@ import javax.net.ssl.SSLException;
 import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
 import org.apache.camel.Exchange;
 import org.apache.camel.Producer;
+import org.apache.camel.component.http.HttpConstants;
 import org.apache.camel.http.base.HttpOperationFailedException;
 import org.apache.camel.support.jsse.KeyStoreParameters;
 import org.apache.camel.support.jsse.SSLContextParameters;
@@ -125,6 +126,7 @@ public class ODataProducerTest extends CamelTestSupport {
         producer.process(exchange);
 
         assertEquals(200, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+        assertNull(exchange.getMessage().getHeader("Authorization"));
         Map<?, ?> body = exchange.getMessage().getBody(Map.class);
         assertNotNull(body);
         assertTrue(body.containsKey("value"));
@@ -212,6 +214,10 @@ public class ODataProducerTest extends CamelTestSupport {
 
         assertEquals(200, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
         assertEquals(1L, 
exchange.getMessage().getHeader(ODataConstants.COUNT));
+        assertNull(exchange.getMessage().getHeader(Exchange.HTTP_URI));
+        assertNull(exchange.getMessage().getHeader(Exchange.HTTP_QUERY));
+        assertNull(exchange.getMessage().getHeader(HttpConstants.HTTP_METHOD));
+        assertNull(exchange.getMessage().getHeader("Accept"));
 
         wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
                 .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
@@ -262,6 +268,8 @@ public class ODataProducerTest extends CamelTestSupport {
         exchange.getMessage().setHeader(Exchange.HTTP_URI, 
"http://malicious.example/other";);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/wrong/path");
         exchange.getMessage().setHeader(Exchange.HTTP_QUERY, 
"unexpected=true");
+        exchange.getMessage().setHeader(HttpConstants.HTTP_METHOD, "POST");
+        exchange.getMessage().setHeader("Accept", "text/plain");
         exchange.getMessage().setHeader(Exchange.HTTP_RAW_QUERY, 
"rawUnexpected=true");
         exchange.getMessage().setHeader("CamelRestHttpUri", 
"http://malicious.example/rest";);
 
@@ -269,7 +277,13 @@ public class ODataProducerTest extends CamelTestSupport {
 
         assertEquals(200, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
 
-        wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+        assertEquals("http://malicious.example/other";, 
exchange.getMessage().getHeader(Exchange.HTTP_URI));
+        assertEquals("unexpected=true", 
exchange.getMessage().getHeader(Exchange.HTTP_QUERY));
+        assertEquals("POST", 
exchange.getMessage().getHeader(HttpConstants.HTTP_METHOD));
+        assertEquals("text/plain", exchange.getMessage().getHeader("Accept"));
+
+        wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+                .withHeader("Accept", equalTo("application/json")));
     }
 
     @Test
@@ -698,7 +712,10 @@ public class ODataProducerTest extends CamelTestSupport {
                     .willReturn(aResponse()
                             .withStatus(200)
                             .withHeader("Content-Type", "application/json")
-                            .withBody("{\"value\":[]}")));
+                            .withHeader("ETag", "W/\"collection-v1\"")
+                            .withHeader("X-Response", "response-value")
+                            .withBody("{\"value\":[],\"@odata.count\":2,"
+                                      + 
"\"@odata.nextLink\":\"http://localhost/odata/Products?$skip=2\"}";)));
 
             // Priority 10: Fallback HTTP Basic 401 challenge with 
WWW-Authenticate header
             wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
@@ -708,9 +725,23 @@ public class ODataProducerTest extends CamelTestSupport {
                             .withHeader("WWW-Authenticate", "Basic 
realm=\"OData Test Realm\"")));
 
             Exchange exchange = authEndpoint.createExchange();
+            exchange.getMessage().setHeader("X-Caller", "caller-value");
+            // keeps WireMock from gzipping the body, so the response headers 
are the plain ones
+            exchange.getMessage().setHeader("Accept-Encoding", "identity");
             authProducer.process(exchange);
 
             assertEquals(200, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+            assertNull(exchange.getMessage().getHeader("Authorization"));
+            // the headers of the delegated HTTP request are not part of the 
result either
+            assertNull(exchange.getMessage().getHeader(Exchange.HTTP_URI));
+            assertNull(exchange.getMessage().getHeader(Exchange.HTTP_QUERY));
+            assertNull(exchange.getMessage().getHeader(Exchange.HTTP_METHOD));
+            assertNull(exchange.getMessage().getHeader("Accept"));
+            assertEquals("caller-value", 
exchange.getMessage().getHeader("X-Caller"));
+            assertEquals("response-value", 
exchange.getMessage().getHeader("X-Response"));
+            assertEquals("W/\"collection-v1\"", 
exchange.getMessage().getHeader(ODataConstants.ETAG));
+            assertEquals(2L, 
exchange.getMessage().getHeader(ODataConstants.COUNT));
+            assertEquals("http://localhost/odata/Products?$skip=2";, 
exchange.getMessage().getHeader(ODataConstants.NEXT_LINK));
             wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
                     .withHeader("Authorization", equalTo(expectedHeader)));
         } finally {
@@ -742,6 +773,7 @@ public class ODataProducerTest extends CamelTestSupport {
             Exchange exchange1 = authEndpoint.createExchange();
             authProducer.process(exchange1);
             assertEquals(200, 
exchange1.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+            assertNull(exchange1.getMessage().getHeader("Authorization"));
             wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
                     .withHeader("Authorization", equalTo("Bearer " + 
configuredToken)));
 
@@ -757,6 +789,7 @@ public class ODataProducerTest extends CamelTestSupport {
             exchange2.getMessage().setHeader("Authorization", 
explicitHeaderToken);
             authProducer.process(exchange2);
             assertEquals(200, 
exchange2.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+            assertEquals(explicitHeaderToken, 
exchange2.getMessage().getHeader("Authorization"));
             wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
                     .withHeader("Authorization", 
equalTo(explicitHeaderToken)));
         } finally {
@@ -764,6 +797,71 @@ public class ODataProducerTest extends CamelTestSupport {
         }
     }
 
+    @Test
+    void testConfiguredRequestHeadersWithEmptyResponse() throws Exception {
+        String configuredToken = "configuredToken";
+        ODataEndpoint configuredEndpoint = context.getEndpoint(
+                "odata:http://localhost:"; + wireMock.getPort()
+                                                               + 
"/odata/Products?authBearerToken=" + configuredToken,
+                ODataEndpoint.class);
+        Producer configuredProducer = configuredEndpoint.createProducer();
+        configuredProducer.start();
+
+        try {
+            wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+                    .withHeader("Authorization", equalTo("Bearer " + 
configuredToken))
+                    .willReturn(aResponse()
+                            .withStatus(204)
+                            .withHeader("ETag", "W/\"collection-v2\"")
+                            .withHeader("X-Response", "response-value")));
+
+            Exchange exchange = configuredEndpoint.createExchange();
+            exchange.getMessage().setHeader("X-Caller", "caller-value");
+            configuredProducer.process(exchange);
+
+            assertEquals(204, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+            assertNull(exchange.getMessage().getBody());
+            assertNull(exchange.getMessage().getHeader("Authorization"));
+            assertEquals("caller-value", 
exchange.getMessage().getHeader("X-Caller"));
+            assertEquals("response-value", 
exchange.getMessage().getHeader("X-Response"));
+            assertEquals("W/\"collection-v2\"", 
exchange.getMessage().getHeader(ODataConstants.ETAG));
+            wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+                    .withHeader("Authorization", equalTo("Bearer " + 
configuredToken)));
+        } finally {
+            configuredProducer.stop();
+        }
+    }
+
+    @Test
+    void testConfiguredRequestHeadersWhenHttpRequestFails() throws Exception {
+        String configuredToken = "configuredToken";
+        ODataEndpoint configuredEndpoint = context.getEndpoint(
+                "odata:http://localhost:"; + wireMock.getPort()
+                                                               + 
"/odata/Products?authBearerToken=" + configuredToken,
+                ODataEndpoint.class);
+        Producer configuredProducer = configuredEndpoint.createProducer();
+        configuredProducer.start();
+
+        try {
+            wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+                    .withHeader("Authorization", equalTo("Bearer " + 
configuredToken))
+                    .willReturn(aResponse().withStatus(500)));
+
+            Exchange exchange = configuredEndpoint.createExchange();
+            exchange.getMessage().setHeader("X-Caller", "caller-value");
+            HttpOperationFailedException exception = assertThrows(
+                    HttpOperationFailedException.class, () -> 
configuredProducer.process(exchange));
+
+            assertEquals(500, exception.getStatusCode());
+            assertNull(exchange.getMessage().getHeader("Authorization"));
+            assertEquals("caller-value", 
exchange.getMessage().getHeader("X-Caller"));
+            wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+                    .withHeader("Authorization", equalTo("Bearer " + 
configuredToken)));
+        } finally {
+            configuredProducer.stop();
+        }
+    }
+
     @Test
     void testBearerEndpointConcurrentIsolation() throws Exception {
         String token1 = "tokenUnit1";
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 e80edda3d353..d072b4110be3 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
@@ -3278,6 +3278,19 @@ now answers with HTTP Status 500 and closes the 
connection. The body is empty, o
 
 A `List` reply is now written as its `toString()`, as a `Map` or a POJO 
already was. Before, no response was sent.
 
+=== camel-odata - result headers
+
+The OData producer no longer adds an `Authorization` header to the result when 
authentication comes from the
+endpoint's `authMethod`, `authUsername`, `authPassword` or `authBearerToken` 
options. A caller-supplied
+`Authorization` header keeps its existing precedence and remains on the 
exchange.
+
+Routes that need an `Authorization` header after the OData step should set it 
explicitly in the route.
+
+The `CamelHttpUri`, `CamelHttpQuery` and `CamelHttpMethod` headers and the 
`Accept` header that the producer sets for
+its own HTTP request are no longer copied to the result either. Before, a 
later HTTP step in the same route reused
+them and sent its request to the OData target, with the OData method. A value 
of these headers that the caller has on
+the exchange is kept.
+
 === camel-odata - the body of CREATE and UPDATE, and a response that is not a 
JSON object
 
 `CREATE` and `UPDATE` send JSON text given as a byte array, a stream or a file 
as it is, as they do for a `String`.

Reply via email to