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

davsclaus pushed a commit to branch cf
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5ab955b4f8c9708dc91faefed5909ae44a72b6e5
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Feb 7 15:12:01 2026 +0100

    CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first 
should use fine grained vertx-web router
---
 .../http/vertx/VertxPlatformHttpConsumer.java      | 101 ++++++++++++++++++---
 ...PlatformHttpRestOpenApiConsumerRestDslTest.java |   2 +-
 .../vertx/PlatformHttpRestOpenApiConsumerTest.java |   1 +
 .../rest/openapi/RestOpenApiProcessor.java         |  25 +++--
 .../camel/component/rest/DefaultRestRegistry.java  |  36 +++++++-
 .../java/org/apache/camel/spi/RestRegistry.java    |  26 ++++++
 .../services/org/apache/camel/model.properties     |   3 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |  10 ++
 8 files changed, 179 insertions(+), 25 deletions(-)

diff --git 
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
 
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
index fd088b7f2a03..3168bd64b4e4 100644
--- 
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
+++ 
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.platform.http.vertx;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -81,7 +82,7 @@ public class VertxPlatformHttpConsumer extends DefaultConsumer
     private final boolean handleWriteResponseError;
     private Set<Method> methods;
     private String path;
-    private Route route;
+    private final List<Route> routes = new ArrayList<>();
     private VertxPlatformHttpRouter router;
     private HttpRequestBodyHandler httpRequestBodyHandler;
     private CookieConfiguration cookieConfiguration;
@@ -135,20 +136,22 @@ public class VertxPlatformHttpConsumer extends 
DefaultConsumer
     protected void doStart() throws Exception {
         super.doStart();
 
-        final Route newRoute = router.route(path);
+        if (startRestServicesContractFirst()) {
+            // rest-dsl contract first using multiple routers per api endpoint
+            return;
+        }
 
+        // standard http consumer using a single router
+        final Route newRoute = router.route(path);
         if (getEndpoint().getRequestTimeout() > 0) {
             
newRoute.handler(TimeoutHandler.create(getEndpoint().getRequestTimeout()));
         }
-
         if 
(getEndpoint().getCamelContext().getRestConfiguration().isEnableCORS() && 
getEndpoint().getConsumes() != null) {
             ((RouteImpl) newRoute).setEmptyBodyPermittedWithConsumes(true);
         }
-
         if (!methods.equals(Method.getAll())) {
             methods.forEach(m -> 
newRoute.method(HttpMethod.valueOf(m.name())));
         }
-
         if (getEndpoint().getComponent().isServerRequestValidation()) {
             if (getEndpoint().getConsumes() != null) {
                 //comma separated contentTypes has to be registered one by one
@@ -163,26 +166,98 @@ public class VertxPlatformHttpConsumer extends 
DefaultConsumer
                 }
             }
         }
-
         httpRequestBodyHandler.configureRoute(newRoute);
         for (Handler<RoutingContext> handler : handlers) {
             newRoute.handler(handler);
         }
-
         newRoute.handler(this::handleRequest);
-
-        this.route = newRoute;
+        this.routes.add(newRoute);
     }
 
     @Override
     protected void doStop() throws Exception {
-        if (route != null) {
-            route.remove();
-            route = null;
-        }
+        this.routes.forEach(Route::remove);
+        this.routes.clear();
         super.doStop();
     }
 
+    /**
+     * Special start logic for Rest DSL with contract-first, which need to use 
fine-grained vertx router to make this
+     * consistent with Camel, otherwise there is only 1 vertx router to handle 
all the API endpoints (coarse grained)
+     * which distorts the observability in vertx and camel-quarkus.
+     *
+     * @return true if in rest-dsl contract-first mode, false if standard mode
+     */
+    protected boolean startRestServicesContractFirst() throws Exception {
+        boolean matched = false;
+        for (var r : 
getEndpoint().getCamelContext().getRestRegistry().listAllRestServices()) {
+            String target = path;
+            if (target.endsWith("*")) {
+                target = target.substring(0, target.length() - 1);
+            }
+            if (r.isContractFirst() && target.equals(r.getBasePath())) {
+                matched = true;
+                // contract-first, then lets build up fine-grained router for 
vertx
+                String u = r.getBasePath() + r.getBaseUrl();
+                // in vertx-web we should replace path parameters from {xxx} 
to :xxx syntax
+                u = u.replaceAll("\\{([a-zA-Z0-9]+)\\}", ":$1");
+                String v = r.getMethod();
+                String c = r.getConsumes();
+                String p = r.getProduces();
+
+                Route sr = router.route(u);
+                sr.method(HttpMethod.valueOf(v));
+                if (getEndpoint().getComponent().isServerRequestValidation()) {
+                    if (c != null) {
+                        for (String cc : c.split(",")) {
+                            sr.consumes(cc);
+                        }
+                    }
+                    if (p != null) {
+                        for (String pp : p.split(",")) {
+                            sr.produces(pp);
+                        }
+                    }
+                }
+                httpRequestBodyHandler.configureRoute(sr);
+                for (Handler<RoutingContext> handler : handlers) {
+                    sr.handler(handler);
+                }
+                sr.handler(this::handleRequest);
+                this.routes.add(sr);
+            }
+        }
+        for (var r : 
getEndpoint().getCamelContext().getRestRegistry().listAllRestSpecifications()) {
+            String target = path;
+            if (target.endsWith("*")) {
+                target = target.substring(0, target.length() - 1);
+            }
+            if (r.isSpecification() && target.equals(r.getBasePath())) {
+                // contract-first, then lets build up fine-grained router for 
vertx
+                String u = r.getBasePath() + r.getBaseUrl();
+                String v = r.getMethod();
+                String p = r.getProduces();
+
+                Route sr = router.route(u);
+                sr.method(HttpMethod.valueOf(v));
+                if (getEndpoint().getComponent().isServerRequestValidation()) {
+                    if (p != null) {
+                        for (String pp : p.split(",")) {
+                            sr.produces(pp);
+                        }
+                    }
+                }
+                httpRequestBodyHandler.configureRoute(sr);
+                for (Handler<RoutingContext> handler : handlers) {
+                    sr.handler(handler);
+                }
+                sr.handler(this::handleRequest);
+                this.routes.add(sr);
+            }
+        }
+        return matched;
+    }
+
     private String configureEndpointPath(PlatformHttpEndpoint endpoint) {
         String path = endpoint.getPath();
         if (endpoint.isMatchOnUriPrefix() && !path.endsWith("*")) {
diff --git 
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
index e320de9ed0f2..6df49cd56ee6 100644
--- 
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
+++ 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
@@ -224,7 +224,7 @@ public class PlatformHttpRestOpenApiConsumerRestDslTest {
             context.start();
 
             given()
-                    .when()
+                    .when().contentType("application/json")
                     .put("/api/v3/pet")
                     .then()
                     .statusCode(400); // no request body
diff --git 
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
index e494ff86b058..74969e7595d1 100644
--- 
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
+++ 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
@@ -231,6 +231,7 @@ public class PlatformHttpRestOpenApiConsumerTest {
 
             given()
                     .when()
+                    .contentType("application/json")
                     .put("/api/v3/pet")
                     .then()
                     .statusCode(400); // no request body
diff --git 
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
 
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
index 531b401a7d00..e1816fcd17cb 100644
--- 
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
+++ 
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
@@ -19,7 +19,6 @@ package org.apache.camel.component.rest.openapi;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Optional;
 
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.Operation;
@@ -184,6 +183,20 @@ public class RestOpenApiProcessor extends 
AsyncProcessorSupport implements Camel
         }
         openApiUtils.clear(); // no longer needed
 
+        // register api-doc in rest registry
+        if (endpoint.getSpecificationUri() != null && apiContextPath != null) {
+            String url = basePath + apiContextPath;
+            String produces = null;
+            if (endpoint.getSpecificationUri().endsWith("json")) {
+                produces = "application/json";
+            } else if (endpoint.getSpecificationUri().endsWith("yaml") || 
endpoint.getSpecificationUri().endsWith("yml")) {
+                produces = "text/yaml";
+            }
+            // register api-doc
+            camelContext.getRestRegistry().addRestSpecification(consumer, 
true, url, apiContextPath, basePath, "GET", produces,
+                    null);
+        }
+
         for (var p : paths) {
             if (p instanceof RestOpenApiConsumerPath rcp) {
                 ServiceHelper.startService(rcp.getBinding());
@@ -216,15 +229,9 @@ public class RestOpenApiProcessor extends 
AsyncProcessorSupport implements Camel
         bc.setClientResponseValidation(config.isClientResponseValidation() || 
endpoint.isClientResponseValidation());
         bc.setEnableNoContentResponse(config.isEnableNoContentResponse());
         bc.setSkipBindingOnErrorCode(config.isSkipBindingOnErrorCode());
-
-        String consumes = 
Optional.ofNullable(openApiUtils.getConsumes(o)).orElse(endpoint.getConsumes());
-        String produces = 
Optional.ofNullable(openApiUtils.getProduces(o)).orElse(endpoint.getProduces());
-
-        bc.setConsumes(consumes);
-        bc.setProduces(produces);
-
+        bc.setConsumes(openApiUtils.getConsumes(o));
+        bc.setProduces(openApiUtils.getProduces(o));
         bc.setRequiredBody(openApiUtils.isRequiredBody(o));
-
         
bc.setRequiredQueryParameters(openApiUtils.getRequiredQueryParameters(o));
         bc.setRequiredHeaders(openApiUtils.getRequiredHeaders(o));
         
bc.setQueryDefaultValues(openApiUtils.getQueryParametersDefaultValue(o));
diff --git 
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
 
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
index ac219ca61d4a..8d50ad31c8b6 100644
--- 
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
+++ 
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
@@ -43,6 +43,7 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
 
     private CamelContext camelContext;
     private final Map<Consumer, List<RestService>> registry = new 
LinkedHashMap<>();
+    private final Map<Consumer, List<RestService>> specs = new 
LinkedHashMap<>();
     private transient Producer apiProducer;
 
     @Override
@@ -51,15 +52,28 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
             String method,
             String consumes, String produces, String inType, String outType, 
String routeId, String description) {
         RestServiceEntry entry = new RestServiceEntry(
-                consumer, contractFirst, url, baseUrl, basePath, uriTemplate, 
method, consumes, produces, inType, outType,
+                consumer, false, contractFirst, url, baseUrl, basePath, 
uriTemplate, method, consumes, produces, inType,
+                outType,
                 description);
         List<RestService> list = registry.computeIfAbsent(consumer, c -> new 
ArrayList<>());
         list.add(entry);
     }
 
+    @Override
+    public void addRestSpecification(
+            Consumer consumer, boolean contractFirst, String url, String 
baseUrl, String basePath, String method,
+            String produces, String description) {
+        RestServiceEntry entry = new RestServiceEntry(
+                consumer, true, contractFirst, url, baseUrl, basePath, null, 
method, null, produces, null, null,
+                description);
+        List<RestService> list = specs.computeIfAbsent(consumer, c -> new 
ArrayList<>());
+        list.add(entry);
+    }
+
     @Override
     public void removeRestService(Consumer consumer) {
         registry.remove(consumer);
+        specs.remove(consumer);
     }
 
     @Override
@@ -71,6 +85,15 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
         return answer;
     }
 
+    @Override
+    public List<RestService> listAllRestSpecifications() {
+        List<RestRegistry.RestService> answer = new ArrayList<>();
+        for (var list : specs.values()) {
+            answer.addAll(list);
+        }
+        return answer;
+    }
+
     @Override
     public int size() {
         int count = 0;
@@ -160,6 +183,7 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
     @Override
     protected void doStop() throws Exception {
         registry.clear();
+        specs.clear();
     }
 
     /**
@@ -168,6 +192,7 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
     private static final class RestServiceEntry implements RestService {
 
         private final Consumer consumer;
+        private final boolean specification;
         private final boolean contractFirst;
         private final String url;
         private final String baseUrl;
@@ -180,10 +205,12 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
         private final String outType;
         private final String description;
 
-        private RestServiceEntry(Consumer consumer, boolean contractFirst, 
String url, String baseUrl, String basePath,
+        private RestServiceEntry(Consumer consumer, boolean specification, 
boolean contractFirst, String url, String baseUrl,
+                                 String basePath,
                                  String uriTemplate, String method, String 
consumes, String produces,
                                  String inType, String outType, String 
description) {
             this.consumer = consumer;
+            this.specification = specification;
             this.contractFirst = contractFirst;
             this.url = url;
             this.baseUrl = baseUrl;
@@ -202,6 +229,11 @@ public class DefaultRestRegistry extends ServiceSupport 
implements RestRegistry,
             return consumer;
         }
 
+        @Override
+        public boolean isSpecification() {
+            return specification;
+        }
+
         @Override
         public boolean isContractFirst() {
             return contractFirst;
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
index 51ba3c1c2882..aad6ad1119bf 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
@@ -37,6 +37,11 @@ public interface RestRegistry extends StaticService {
          */
         Consumer getConsumer();
 
+        /**
+         * Is this the API contract specification (ie api-doc)
+         */
+        boolean isSpecification();
+
         /**
          * Is the rest service based on code-first or contract-first
          */
@@ -139,6 +144,27 @@ public interface RestRegistry extends StaticService {
      */
     List<RestService> listAllRestServices();
 
+    /**
+     * Adds information about the API specification (ie api-doc)
+     *
+     * @param consumer      the consumer
+     * @param contractFirst is the rest service based on code-first or 
contract-first
+     * @param url           the absolute url of the REST service
+     * @param baseUrl       the base url of the REST service
+     * @param basePath      the base path
+     * @param method        the HTTP method
+     * @param produces      optional details about what media-types the REST 
service returns
+     * @param description   optional description about the service
+     */
+    void addRestSpecification(Consumer consumer, boolean contractFirst, String 
url, String baseUrl, String basePath, String method, String produces, String 
description);
+
+    /**
+     * List all REST API specification (ie api-doc)
+     *
+     * @return all the API specification (ie api-doc)
+     */
+    List<RestService> listAllRestSpecifications();
+
     /**
      * Number of rest services in the registry.
      *
diff --git 
a/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
 
b/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
index 2607d21e1e52..9a641b9986d3 100644
--- 
a/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
+++ 
b/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
@@ -222,6 +222,7 @@ threads
 thrift
 throttle
 throwException
+tidyMarkup
 to
 toD
 tokenize
@@ -231,6 +232,7 @@ transacted
 transform
 transformDataType
 transformers
+typeFilter
 univocityCsv
 univocityFixed
 univocityHeader
@@ -243,6 +245,7 @@ variable
 wasm
 weightedLoadBalancer
 when
+whenSkipSendToEndpoint
 wireTap
 xmlSecurity
 xpath
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index 605ac9179024..5c5a2f90c47d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -42,6 +42,16 @@ Even if the interface HostApplicationEventHandler is public, 
I do not expect Cam
 
 Consequently, there is an API break 
`org.apache.camel.tahu.handlers.TahuHostApplicationEventHandler` has been 
removed. It is replaced by 
`org.apache.camel.tahu.handlers.MultiTahuHostApplicationEventHandler`.
 
+=== camel-platform-http-vertx and Rest DSL contract-first
+
+When using Rest DSL in _contract first_ style, then the HTTP engine 
(vertx-web) instead of a single
+router to handle all incoming Rest API calls, is now one unique router per API 
endpoint. This change
+can affect HTTP request validation as vertx/Quarkus is now also performing 
this per API endpoint according
+to the API specification.
+
+All together this would make Camel behave similar for Rest DSL for both _code 
first_ and _contract first_ style.
+
+
 === Component deprecation
 
 The `camel-olingo2` and `camel-olingo4` component are deprecated.

Reply via email to