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

davsclaus 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 527f6dea5f68 CAMEL-26463: camel-undertow - a consumer that never 
started or failed to start unregisters only what it registered (#29125)
527f6dea5f68 is described below

commit 527f6dea5f68c14660ca96fd1a2251c7d1adea26
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Nov 4 15:05:03 2026 +0530

    CAMEL-26463: camel-undertow - a consumer that never started or failed to 
start unregisters only what it registered (#29125)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../component/undertow/UndertowComponent.java      |  12 +-
 .../camel/component/undertow/UndertowConsumer.java |  18 +-
 .../camel/component/undertow/UndertowProducer.java |  10 +-
 .../undertow/handlers/CamelMethodHandler.java      |  15 +-
 .../UndertowConsumerNotStartedUnregisterTest.java  | 243 +++++++++++++++++++++
 5 files changed, 286 insertions(+), 12 deletions(-)

diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowComponent.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowComponent.java
index 96c4d325f705..5836706ae561 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowComponent.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowComponent.java
@@ -382,7 +382,6 @@ public class UndertowComponent extends DefaultComponent
         final UndertowHost host = undertowRegistry.computeIfAbsent(key, 
this::createUndertowHost);
 
         host.validateEndpointURI(uri);
-        handlers.add(registrationInfo);
 
         HttpHandler handlerWrapped = handler;
         if (handler instanceof CamelWebSocketHandler webSocketHandler) {
@@ -396,7 +395,16 @@ public class UndertowComponent extends DefaultComponent
             handlerWrapped = this.securityProvider.wrapHttpHandler(handler);
         }
 
-        return host.registerHandler(endpoint, consumer, registrationInfo, 
handlerWrapped);
+        final boolean added = handlers.add(registrationInfo);
+        try {
+            return host.registerHandler(endpoint, consumer, registrationInfo, 
handlerWrapped);
+        } catch (RuntimeException e) {
+            // a failed registration is not unregistered, so its info goes now 
(unless another endpoint added it)
+            if (added) {
+                handlers.remove(registrationInfo);
+            }
+            throw e;
+        }
     }
 
     public void unregisterEndpoint(
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
index 6f5072afa2aa..25c72927685b 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
@@ -76,6 +76,10 @@ public class UndertowConsumer extends DefaultConsumer 
implements HttpHandler, Su
     private CamelWebSocketHandler webSocketHandler;
     private boolean rest;
     private volatile boolean suspended;
+    // what doStart registered, so doStop undoes only that: a consumer that 
never started, or whose start failed, must
+    // not unregister the path of another consumer, nor unset the consumer of 
a shared WebSocket handler
+    private boolean registered;
+    private boolean webSocketConsumerSet;
 
     public UndertowConsumer(UndertowEndpoint endpoint, Processor processor) {
         super(endpoint, processor);
@@ -115,9 +119,11 @@ public class UndertowConsumer extends DefaultConsumer 
implements HttpHandler, Su
              */
             this.webSocketHandler = (CamelWebSocketHandler) 
endpoint.getComponent().registerEndpoint(this,
                     endpoint.getHttpHandlerRegistrationInfo(), 
endpoint.getSslContext(), new CamelWebSocketHandler());
+            this.registered = true;
             // the access log and the custom handlers run before the upgrade, 
as they run before an HTTP request
             this.webSocketHandler.setConsumer(this,
                     
wrapWithAccessLogAndHandlers(this.webSocketHandler.getUpgradeHandler(), 
endpoint));
+            this.webSocketConsumerSet = true;
         } else {
             // allow for HTTP 1.1 continue
             // form fields of a request that declares no charset are UTF-8 (as 
browsers send them), not the
@@ -135,6 +141,7 @@ public class UndertowConsumer extends DefaultConsumer 
implements HttpHandler, Su
                     Handlers.httpContinueRead(
                             // wrap with EagerFormParsingHandler to enable 
undertow form parsers
                             httpHandler));
+            this.registered = true;
         }
     }
 
@@ -163,11 +170,16 @@ public class UndertowConsumer extends DefaultConsumer 
implements HttpHandler, Su
     protected void doStop() throws Exception {
         this.suspended = false;
         super.doStop();
-        if (this.webSocketHandler != null) {
+        if (this.webSocketConsumerSet) {
+            this.webSocketConsumerSet = false;
             this.webSocketHandler.setConsumer(null);
         }
-        UndertowEndpoint endpoint = getEndpoint();
-        endpoint.getComponent().unregisterEndpoint(this, 
endpoint.getHttpHandlerRegistrationInfo(), endpoint.getSslContext());
+        if (this.registered) {
+            this.registered = false;
+            UndertowEndpoint endpoint = getEndpoint();
+            endpoint.getComponent().unregisterEndpoint(this, 
endpoint.getHttpHandlerRegistrationInfo(),
+                    endpoint.getSslContext());
+        }
     }
 
     @Override
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
index 6ede7c60a62c..0a2ee29d4813 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
@@ -71,6 +71,8 @@ public class UndertowProducer extends DefaultAsyncProducer {
     private XnioSsl ssl;
     private XnioWorker worker;
     private CamelWebSocketHandler webSocketHandler;
+    // whether this producer registered its WebSocket handler, so only then it 
unregisters it
+    private boolean registered;
 
     public UndertowProducer(final UndertowEndpoint endpoint, final OptionMap 
options) {
         super(endpoint);
@@ -278,6 +280,7 @@ public class UndertowProducer extends DefaultAsyncProducer {
         if (endpoint.isWebSocket()) {
             this.webSocketHandler = (CamelWebSocketHandler) 
endpoint.getComponent().registerEndpoint(endpoint, null,
                     endpoint.getHttpHandlerRegistrationInfo(), 
endpoint.getSslContext(), new CamelWebSocketHandler());
+            this.registered = true;
             this.webSocketHandler.addProducer(endpoint);
         }
 
@@ -288,10 +291,9 @@ public class UndertowProducer extends DefaultAsyncProducer 
{
     protected void doStop() throws Exception {
         super.doStop();
 
-        if (endpoint.isWebSocket()) {
-            if (webSocketHandler != null) {
-                webSocketHandler.removeProducer(endpoint);
-            }
+        if (registered) {
+            registered = false;
+            webSocketHandler.removeProducer(endpoint);
             endpoint.getComponent().unregisterEndpoint(null, 
endpoint.getHttpHandlerRegistrationInfo(),
                     endpoint.getSslContext());
         }
diff --git 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelMethodHandler.java
 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelMethodHandler.java
index 6c19a8cb94c3..bfb74a8c6c66 100644
--- 
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelMethodHandler.java
+++ 
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/handlers/CamelMethodHandler.java
@@ -86,9 +86,18 @@ public class CamelMethodHandler implements HttpHandler {
         HttpHandler result = null;
         methodMapLock.lock(); // we lock to get a reliable sum of refCounts in 
remove(String)
         try {
-            for (String method : splitMethods(methods)) {
-                MethodEntry en = methodMap.computeIfAbsent(method, m -> new 
MethodEntry());
-                result = en.addRef(handler, method);
+            final String[] split = splitMethods(methods);
+            for (int i = 0; i < split.length; i++) {
+                MethodEntry en = methodMap.computeIfAbsent(split[i], m -> new 
MethodEntry());
+                try {
+                    result = en.addRef(handler, split[i]);
+                } catch (IllegalArgumentException e) {
+                    // all or nothing, as a failed registration is not 
unregistered
+                    for (int j = 0; j < i; j++) {
+                        methodMap.get(split[j]).removeRef();
+                    }
+                    throw e;
+                }
             }
         } finally {
             methodMapLock.unlock();
diff --git 
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowConsumerNotStartedUnregisterTest.java
 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowConsumerNotStartedUnregisterTest.java
new file mode 100644
index 000000000000..5a2e18443a17
--- /dev/null
+++ 
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowConsumerNotStartedUnregisterTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.undertow;
+
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.FailedToStartRouteException;
+import org.apache.camel.Producer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.infra.common.http.WebsocketTestClient;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * An undertow consumer (or WebSocket producer) that never started, or whose 
start failed, must not unregister the
+ * handler of a running consumer of the same path when it is stopped or its 
route is removed.
+ */
+class UndertowConsumerNotStartedUnregisterTest extends BaseUndertowTest {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:http://localhost:{{port}}/dup";).routeId("running").transform().constant("running");
+            }
+        };
+    }
+
+    @Test
+    void controlRestartingTheRunningRoute() throws Exception {
+        // control: a consumer that stops unregisters its path, and registers 
it again when it starts again
+        addRoute("undertow:http://localhost:{{port}}/other";, "other");
+        assertEquals("running", get("/dup"));
+
+        context.getRouteController().stopRoute("running");
+        assertEquals("No matching path found", get("/dup"));
+        context.getRouteController().startRoute("running");
+
+        assertEquals("running", get("/dup"));
+        context.getRouteController().stopRoute("running");
+        context.getRouteController().startRoute("running");
+        assertEquals("running", get("/dup"));
+    }
+
+    @Test
+    void removingANeverStartedRouteKeepsThePathOfTheRunningRoute() throws 
Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:http://localhost:{{port}}/other";).routeId("other").transform().constant("other");
+                
from("undertow:http://localhost:{{port}}/dup";).routeId("idle").autoStartup(false)
+                        .transform().constant("idle");
+            }
+        });
+        assertEquals("running", get("/dup"));
+
+        context.removeRoute("idle");
+
+        assertEquals("running", get("/dup"));
+        assertEquals("other", get("/other"));
+    }
+
+    @Test
+    void removingANeverStartedRouteKeepsAStaticPathNextToATemplatePath() 
throws Exception {
+        // the second case of the JIRA, with CAMEL-26460: a template path 
/orders/{id} and a static /orders are running,
+        // and a third /orders route that never started is removed: both paths 
still answer
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:http://localhost:{{port}}/orders/{id}";).routeId("template")
+                        .transform().constant("template");
+                
from("undertow:http://localhost:{{port}}/orders";).routeId("static").transform().constant("static");
+                
from("undertow:http://localhost:{{port}}/orders";).routeId("idle").autoStartup(false)
+                        .transform().constant("idle");
+            }
+        });
+        assertEquals("static", get("/orders"));
+        assertEquals("template", get("/orders/1"));
+
+        context.removeRoute("idle");
+
+        assertEquals("static", get("/orders"));
+        assertEquals("template", get("/orders/1"));
+    }
+
+    @Test
+    void removingANeverStartedRouteKeepsTheServerOfTheRunningRoute() throws 
Exception {
+        // the path of the running route is the only path of the server: the 
server must not stop
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:http://localhost:{{port}}/dup";).routeId("idle").autoStartup(false)
+                        .transform().constant("idle");
+            }
+        });
+        assertEquals("running", get("/dup"));
+
+        context.removeRoute("idle");
+
+        assertEquals("running", get("/dup"));
+    }
+
+    @Test
+    void removingARouteWhoseStartFailedKeepsThePathOfTheRunningRoute() throws 
Exception {
+        // a second consumer of the same path and method (another endpoint, as 
one endpoint cannot have two consumers)
+        // fails to start, as the path already has a handler
+        
assertStartFails("undertow:http://localhost:{{port}}/dup?matchOnUriPrefix=false";,
 "failed",
+                "Duplicate handler for ");
+        assertEquals("running", get("/dup"));
+
+        context.getRouteController().stopRoute("failed");
+        context.removeRoute("failed");
+
+        assertEquals("running", get("/dup"));
+    }
+
+    @Test
+    void routeWhoseStartFailedStartsOnceThePathIsFree() throws Exception {
+        
assertStartFails("undertow:http://localhost:{{port}}/dup?matchOnUriPrefix=false";,
 "failed",
+                "Duplicate handler for ");
+        // a second start while the path is still taken fails too, and keeps 
the path of the running route
+        assertThrows(FailedToStartRouteException.class, () -> 
context.getRouteController().startRoute("failed"));
+        assertEquals("running", get("/dup"));
+
+        context.getRouteController().stopRoute("running");
+        context.getRouteController().startRoute("failed");
+
+        assertEquals("failed", get("/dup"));
+    }
+
+    @Test
+    void removingARouteWhoseStartFailedWithSeveralMethodsKeepsNoMethod() 
throws Exception {
+        
addRoute("undertow:http://localhost:{{port}}/mm?httpMethodRestrict=POST,OPTIONS";,
 "post");
+        // GET is free, but POST already has a handler, so the start fails: 
GET must not stay registered
+        
assertStartFails("undertow:http://localhost:{{port}}/mm?httpMethodRestrict=GET,POST";,
 "failed",
+                "Duplicate handler for POST method");
+        context.getRouteController().stopRoute("failed");
+        context.removeRoute("failed");
+
+        // nor in the methods that OPTIONS lists
+        assertEquals("POST,OPTIONS", call("/mm", 
"OPTIONS").getMessage().getHeader("Allow"));
+        // a route for GET alone can start, and the route for POST still 
answers
+        
addRoute("undertow:http://localhost:{{port}}/mm?httpMethodRestrict=GET";, "get");
+        assertEquals("get", get("/mm"));
+        assertEquals("post", call("/mm", 
"POST").getMessage().getBody(String.class));
+    }
+
+    @Test
+    void removingAWebSocketRouteWhoseStartFailedKeepsTheRunningConsumer() 
throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:ws://localhost:{{port}}/ws").routeId("ws").to("mock:ws");
+            }
+        });
+        // a second consumer of the same WebSocket path fails to start, as the 
WebSocket handler of the path already has
+        // a consumer (sendTimeout only makes it another endpoint, as one 
endpoint cannot have two consumers)
+        
assertStartFails("undertow:ws://localhost:{{port}}/ws?sendTimeout=30001", 
"ws-failed", "setConsumer");
+        context.getRouteController().stopRoute("ws-failed");
+        context.removeRoute("ws-failed");
+
+        assertWebSocketRouteReceives("Hello");
+
+        // the failed consumer had registered with the shared WebSocket 
handler of the path, and unregistered when it
+        // was removed: once the running route is removed too, the path is gone
+        context.getRouteController().stopRoute("ws");
+        context.removeRoute("ws");
+        assertThrows(CompletionException.class,
+                () -> new WebsocketTestClient("ws://localhost:" + getPort() + 
"/ws").connect());
+    }
+
+    @Test
+    void stoppingAWebSocketProducerThatNeverStartedKeepsTheRunningConsumer() 
throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("undertow:ws://localhost:{{port}}/ws").routeId("ws").to("mock:ws");
+            }
+        });
+        Producer producer = 
context.getEndpoint("undertow:ws://localhost:{{port}}/ws").createProducer();
+        producer.stop();
+
+        assertWebSocketRouteReceives("Hello");
+    }
+
+    private void addRoute(String uri, String routeId) throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(uri).routeId(routeId).transform().constant(routeId);
+            }
+        });
+    }
+
+    private void assertStartFails(String uri, String routeId, String message) {
+        FailedToStartRouteException e = 
assertThrows(FailedToStartRouteException.class, () -> addRoute(uri, routeId));
+        assertTrue(e.getMessage().contains(message), e.getMessage());
+    }
+
+    private String get(String path) {
+        return template.requestBody("undertow:http://localhost:{{port}}"; + 
path + "?throwExceptionOnFailure=false",
+                null, String.class);
+    }
+
+    private Exchange call(String path, String method) {
+        return template.request("undertow:http://localhost:{{port}}"; + path + 
"?throwExceptionOnFailure=false",
+                e -> e.getIn().setHeader(Exchange.HTTP_METHOD, method));
+    }
+
+    private void assertWebSocketRouteReceives(String message) throws Exception 
{
+        MockEndpoint ws = getMockEndpoint("mock:ws");
+        ws.expectedBodiesReceived(message);
+        WebsocketTestClient client = new WebsocketTestClient("ws://localhost:" 
+ getPort() + "/ws");
+        client.connect();
+        try {
+            client.sendTextMessage(message);
+            MockEndpoint.assertIsSatisfied(context, 10, TimeUnit.SECONDS);
+        } finally {
+            client.close();
+        }
+    }
+}

Reply via email to