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

davsclaus pushed a commit to branch CAMEL-26487-netty-http-equals-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3f5916a39d6d7a74b7b89a090debb86fd1639183
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 15:17:07 2026 +0100

    CAMEL-26487: camel-netty-http - endpoints that differ only in their options 
are not equal
    
    NettyHttpEndpoint compares the full endpoint URI, with the endpoint 
options, in equals and
    hashCode. Endpoints that share an HTTP address but differ in their options 
then get a producer
    of their own in a ProducerTemplate, toD or another EIP with a dynamic URI. 
The endpoint URI is
    unchanged (the HTTP address), so JMX names, auto-mock names, tracing and 
metrics are the same.
    
    On main the endpoint URI itself became the full URI (#28863); this is the 
backport of the
    behaviour without that visible change.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../component/netty/http/NettyHttpComponent.java   |  2 +
 .../component/netty/http/NettyHttpEndpoint.java    | 29 +++++++
 .../NettyHttpEndpointDifferentOptionsTest.java     | 90 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    | 14 ++++
 4 files changed, 135 insertions(+)

diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpComponent.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpComponent.java
index 8427a21a5f26..0e284bfddded 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpComponent.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpComponent.java
@@ -182,6 +182,8 @@ public class NettyHttpComponent extends NettyComponent
         String addressUri = URISupport.createRemainingURI(u, 
parameters).toString();
 
         NettyHttpEndpoint answer = new NettyHttpEndpoint(addressUri, this, 
config);
+        // endpoints that differ only in their options must not be equal, so 
the full uri is the identity
+        answer.setIdentityUri(uri);
         answer.getConfiguration().setMuteException(muteException);
         setProperties(answer, parameters);
 
diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpEndpoint.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpEndpoint.java
index bd2dd0b0a13b..b007ac75727b 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpEndpoint.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpEndpoint.java
@@ -59,6 +59,9 @@ public class NettyHttpEndpoint extends NettyEndpoint 
implements HeaderFilterStra
 
     private OAuthHttpSecuritySupport oauthHttpSecurity;
 
+    // the full uri with the endpoint options, which is the identity of this 
endpoint in equals and hashCode
+    private String identityUri;
+
     @UriParam
     private NettyHttpConfiguration configuration;
     @UriParam(label = "advanced", name = "configuration",
@@ -315,6 +318,32 @@ public class NettyHttpEndpoint extends NettyEndpoint 
implements HeaderFilterStra
         oauthHttpSecurity = OAuthHttpSecuritySupport.create(getCamelContext(), 
oauthProfile);
     }
 
+    void setIdentityUri(String identityUri) {
+        this.identityUri = identityUri;
+    }
+
+    private String getIdentityUri() {
+        return identityUri != null ? identityUri : getEndpointUri();
+    }
+
+    @Override
+    public int hashCode() {
+        return getIdentityUri().hashCode() * 37 + 1;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object instanceof NettyHttpEndpoint that) {
+            // the endpoint uri is the http address, which endpoints that 
differ only in their options share,
+            // so compare the full uri; and as DefaultEndpoint, the 
CamelContext must also be the same
+            String thisContextName = this.getCamelContext() != null ? 
this.getCamelContext().getName() : null;
+            String thatContextName = that.getCamelContext() != null ? 
that.getCamelContext().getName() : null;
+            return ObjectHelper.equal(this.getIdentityUri(), 
that.getIdentityUri())
+                    && ObjectHelper.equal(thisContextName, thatContextName);
+        }
+        return false;
+    }
+
     private boolean isProxyProtocol() {
         URI baseUri = URI.create(getEndpointBaseUri());
         String protocol = baseUri.getScheme();
diff --git 
a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpEndpointDifferentOptionsTest.java
 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpEndpointDifferentOptionsTest.java
new file mode 100644
index 000000000000..0d24e7384685
--- /dev/null
+++ 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpEndpointDifferentOptionsTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.component.netty.http;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class NettyHttpEndpointDifferentOptionsTest extends 
BaseNettyTestSupport {
+
+    @Test
+    public void testEndpointsWithDifferentOptionsAreNotEqual() {
+        Endpoint plain = 
context.getEndpoint("netty-http:http://localhost:{{port}}/foo?bar=123";);
+        Endpoint option = 
context.getEndpoint("netty-http:http://localhost:{{port}}/foo?bar=123&throwExceptionOnFailure=false";);
+
+        assertNotEquals(plain, option);
+        assertNotEquals(plain.hashCode(), option.hashCode());
+        // the endpoint uri is unchanged: the http address
+        assertEquals(plain.getEndpointUri(), option.getEndpointUri());
+
+        // the same uri is the same endpoint
+        assertEquals(plain, 
context.getEndpoint("netty-http:http://localhost:{{port}}/foo?bar=123";));
+
+        // both send to the same address, which keeps the query parameters 
that are not endpoint options
+        String url = "http://localhost:"; + getPort() + "/foo?bar=123";
+        assertEquals(url, NettyHttpHelper.createURL(new 
DefaultExchange(context), (NettyHttpEndpoint) plain));
+        assertEquals(url, NettyHttpHelper.createURL(new 
DefaultExchange(context), (NettyHttpEndpoint) option));
+    }
+
+    @Test
+    public void testProducerTemplateUsesTheOptionsOfEachEndpoint() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> 
template.requestBody("netty-http:http://localhost:{{port}}/fail";, "Hello 
World", String.class));
+        NettyHttpOperationFailedException cause = 
assertIsInstanceOf(NettyHttpOperationFailedException.class, e.getCause());
+        assertEquals(500, cause.getStatusCode());
+
+        // the same address with another option is another endpoint, with a 
producer of its own
+        String out = 
template.requestBody("netty-http:http://localhost:{{port}}/fail?throwExceptionOnFailure=false";,
+                "Hello World", String.class);
+        assertEquals("Failed", out);
+    }
+
+    @Test
+    public void testToDUsesTheOptionsOfEachEndpoint() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.requestBodyAndHeader("direct:start", "Hello 
World", "throwOnFailure", "true", String.class));
+        NettyHttpOperationFailedException cause = 
assertIsInstanceOf(NettyHttpOperationFailedException.class, e.getCause());
+        assertEquals(500, cause.getStatusCode());
+
+        String out = template.requestBodyAndHeader("direct:start", "Hello 
World", "throwOnFailure", "false", String.class);
+        assertEquals("Failed", out);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("netty-http:http://0.0.0.0:{{port}}/fail";)
+                        .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
+                        .transform().constant("Failed");
+
+                from("direct:start")
+                        
.toD("netty-http:http://localhost:{{port}}/fail?throwExceptionOnFailure=${header.throwOnFailure}";);
+            }
+        };
+    }
+}
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 75842677ea21..eea0714c72ee 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
@@ -155,6 +155,20 @@ credentials. A deployment that counted on the inherited 
list as a restriction ha
 A consumer whose `requestUriPattern` is `/` now also uses its own keys, 
certificates and asynchronous MDN settings when
 another consumer created the shared server connection; before, it used the 
first consumer's.
 
+=== camel-netty-http - endpoints that differ only in their options are 
distinct endpoints
+
+Two `netty-http` endpoints whose URIs differ only in their endpoint options, 
for example
+`netty-http:http://localhost:8080/foo` and 
`netty-http:http://localhost:8080/foo?throwExceptionOnFailure=false`, are no
+longer equal. Each of them now gets its own producer when a 
`ProducerTemplate`, `toD` or another EIP with a dynamic URI
+sends to them. Before, these endpoints used the producer of the endpoint that 
was used first, with the options of that
+endpoint.
+
+The endpoint URI does not change: it is still the HTTP address, such as 
`http://localhost:8080/foo`, so JMX names,
+auto-mock names, tracing and metrics are the same as before.
+
+Two routes that consume from the same address with different endpoint options 
are no longer rejected at startup as
+"Multiple consumers for the same endpoint is not allowed". Use one consumer 
per address.
+
 == Upgrading from 4.22.1 to 4.22.2
 
 === camel-management - Route group exchange counters count once per entry into 
the group again

Reply via email to