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();
+ }
+ }
+}