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`.