From 0467923826c6c2a0b90a3a05c039de458f26a6bb Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Tue, 8 Sep 2026 11:23:37 +0100 Subject: [PATCH 1/6] CAMEL-24649: Add unmatched request handler to REST OpenAPI component --- .../catalog/docs/rest-openapi-component.adoc | 34 +++ .../src/main/docs/rest-openapi-component.adoc | 34 +++ ...ultRestOpenApiUnmatchedRequestHandler.java | 38 ++++ .../rest/openapi/RestOpenApiProcessor.java | 30 ++- .../RestOpenApiUnmatchedRequestHandler.java | 44 ++++ ...estOpenApiUnmatchedRequestHandlerTest.java | 198 ++++++++++++++++++ .../resources/unmatched-request-handler.yaml | 42 ++++ 7 files changed, 413 insertions(+), 7 deletions(-) create mode 100644 components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java create mode 100644 components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandler.java create mode 100644 components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java create mode 100644 components/camel-rest-openapi/src/test/resources/unmatched-request-handler.yaml diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc index bc999a9da000a..7fd566da64be8 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc @@ -198,6 +198,40 @@ If any of the validation checks fail, then a `RestOpenApiValidationException` is has a `getValidationErrors` method that returns the error messages from the validator. +== Unmatched requests + +By default, an incoming request that does not match any operation in the OpenAPI specification is answered +with HTTP 404, and a request that matches a path but not the HTTP method is answered with HTTP 405 and an +`Allow` header listing the allowed methods. In both cases the response body is empty. + +To return a custom response, for example a JSON error body, register a bean in the +xref:manual::registry.adoc[Registry] that implements the `RestOpenApiUnmatchedRequestHandler` interface. +The handler is called with the exchange, the status code (`404` or `405`) and the list of allowed HTTP +methods (empty for `404`), and can then set the response body, status code and headers as needed. When no +handler is registered, the default (empty body) response is used. + +[source,java] +---- +public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler { + + @Override + public void handle(Exchange exchange, int statusCode, List allowedMethods) { + String message = statusCode == 404 + ? "The requested resource was not found." + : "The HTTP method is not allowed for this resource."; + + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); + exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json"); + if (!allowedMethods.isEmpty()) { + exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods)); + } + exchange.getMessage().setBody(String.format( + "{\"message\":\"%s\"}", message)); + } +} +---- + + == Examples === PetStore diff --git a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc index bc999a9da000a..7fd566da64be8 100644 --- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc +++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc @@ -198,6 +198,40 @@ If any of the validation checks fail, then a `RestOpenApiValidationException` is has a `getValidationErrors` method that returns the error messages from the validator. +== Unmatched requests + +By default, an incoming request that does not match any operation in the OpenAPI specification is answered +with HTTP 404, and a request that matches a path but not the HTTP method is answered with HTTP 405 and an +`Allow` header listing the allowed methods. In both cases the response body is empty. + +To return a custom response, for example a JSON error body, register a bean in the +xref:manual::registry.adoc[Registry] that implements the `RestOpenApiUnmatchedRequestHandler` interface. +The handler is called with the exchange, the status code (`404` or `405`) and the list of allowed HTTP +methods (empty for `404`), and can then set the response body, status code and headers as needed. When no +handler is registered, the default (empty body) response is used. + +[source,java] +---- +public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler { + + @Override + public void handle(Exchange exchange, int statusCode, List allowedMethods) { + String message = statusCode == 404 + ? "The requested resource was not found." + : "The HTTP method is not allowed for this resource."; + + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); + exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json"); + if (!allowedMethods.isEmpty()) { + exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods)); + } + exchange.getMessage().setBody(String.format( + "{\"message\":\"%s\"}", message)); + } +} +---- + + == Examples === PetStore diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java new file mode 100644 index 0000000000000..93cceabae99a6 --- /dev/null +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java @@ -0,0 +1,38 @@ +/* + * 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.rest.openapi; + +import java.util.List; + +import org.apache.camel.Exchange; + +/** + * Default {@link RestOpenApiUnmatchedRequestHandler} that returns empty body with the HTTP status code and, if + * provided, the {@code Allow} header. + * + * @since 4.23 + */ +public class DefaultRestOpenApiUnmatchedRequestHandler implements RestOpenApiUnmatchedRequestHandler { + + @Override + public void handle(Exchange exchange, int statusCode, List allowedMethods) { + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); + if (!allowedMethods.isEmpty()) { + exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods)); + } + } +} 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 e46114cd54bdd..e26b3319f2816 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,6 +19,7 @@ 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; @@ -28,7 +29,9 @@ import org.apache.camel.spi.RestConfiguration; import org.apache.camel.spi.RestRegistry; import org.apache.camel.support.AsyncProcessorSupport; +import org.apache.camel.support.CamelContextHelper; import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResolverHelper; import org.apache.camel.support.RestConsumerContextPathMatcher; import org.apache.camel.support.processor.RestBindingAdvice; import org.apache.camel.support.processor.RestBindingAdviceFactory; @@ -47,6 +50,7 @@ public class RestOpenApiProcessor extends AsyncProcessorSupport implements Camel private final String apiContextPath; private final List> paths = new ArrayList<>(); private final RestOpenapiProcessorStrategy restOpenapiProcessorStrategy; + private RestOpenApiUnmatchedRequestHandler unmatchedRequestHandler; private PlatformHttpConsumerAware platformHttpConsumer; private Consumer consumer; private OpenApiUtils openApiUtils; @@ -130,13 +134,8 @@ public boolean process(Exchange exchange, AsyncCallback callback) { final String contextPath = path; List allow = METHODS.stream() .filter(v -> RestConsumerContextPathMatcher.matchBestPath(v, contextPath, paths) != null).toList(); - if (allow.isEmpty()) { - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 404); - } else { - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 405); - // include list of allowed VERBs - exchange.getMessage().setHeader("Allow", String.join(", ", allow)); - } + int statusCode = allow.isEmpty() ? 404 : 405; + unmatchedRequestHandler.handle(exchange, statusCode, allow); exchange.setRouteStop(true); callback.done(true); return true; @@ -156,6 +155,7 @@ public void afterPropertiesConfigured(CamelContext camelContext) { this.openApiUtils = new OpenApiUtils(camelContext, endpoint.getBindingPackageScan(), openAPI.getComponents()); this.restRegistry = PluginHelper.getRestRegistry(camelContext); + this.unmatchedRequestHandler = lookupUnmatchedRequestHandler(camelContext); // register all openapi paths for (var e : openAPI.getPaths().entrySet()) { String path = e.getKey(); // path @@ -229,6 +229,22 @@ public void afterPropertiesConfigured(CamelContext camelContext) { ServiceHelper.startService(restOpenapiProcessorStrategy); } + private static RestOpenApiUnmatchedRequestHandler lookupUnmatchedRequestHandler(CamelContext camelContext) { + RestOpenApiUnmatchedRequestHandler answer + = CamelContextHelper.findSingleByType(camelContext, RestOpenApiUnmatchedRequestHandler.class); + if (answer == null) { + // lookup via classpath to find custom factory + Optional result = ResolverHelper.resolveService( + camelContext, + camelContext.getCamelContextExtension().getBootstrapFactoryFinder(), + RestOpenApiUnmatchedRequestHandler.FACTORY, + RestOpenApiUnmatchedRequestHandler.class); + // else use a default implementation + answer = result.orElseGet(DefaultRestOpenApiUnmatchedRequestHandler::new); + } + return answer; + } + private RestBindingConfiguration createRestBindingConfiguration(Operation o) { RestConfiguration config = camelContext.getRestConfiguration(); RestConfiguration.RestBindingMode mode = config.getBindingMode(); diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandler.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandler.java new file mode 100644 index 0000000000000..04bdbcf7a02c8 --- /dev/null +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandler.java @@ -0,0 +1,44 @@ +/* + * 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.rest.openapi; + +import java.util.List; + +import org.apache.camel.Exchange; + +/** + * Used for customizing the HTTP 404 and 405 responses when an incoming request does not match any operation defined in + * the OpenAPI specification. + *

+ * This allows to plugin different handlers to produce custom error response bodies. + * + * @see DefaultRestOpenApiUnmatchedRequestHandler + * @since 4.23 + */ +public interface RestOpenApiUnmatchedRequestHandler { + + String FACTORY = "rest-openapi-unmatched-request-handler-factory"; + + /** + * Handles the incoming request that did not match any operation in the OpenAPI specification. + * + * @param exchange the current exchange + * @param statusCode the HTTP status code (404 or 405) + * @param allowedMethods the list of allowed HTTP methods for the requested path (empty for 404) + */ + void handle(Exchange exchange, int statusCode, List allowedMethods); +} diff --git a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java new file mode 100644 index 0000000000000..c45a9c9a33aa4 --- /dev/null +++ b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java @@ -0,0 +1,198 @@ +/* + * 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.rest.openapi; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.models.OpenAPI; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.platform.http.PlatformHttpComponent; +import org.apache.camel.component.platform.http.PlatformHttpEndpoint; +import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer; +import org.apache.camel.component.platform.http.spi.PlatformHttpConsumerAware; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultExchange; +import org.apache.camel.support.service.ServiceHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RestOpenApiUnmatchedRequestHandlerTest extends ManagedCamelTestSupport { + + private CamelContext camelContext; + private RestOpenApiProcessor openApiProcessor; + + @BeforeEach + public void createMocks() throws Exception { + initializeContextForComponent("rest-openapi"); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:listUsers").to("mock:listUsers"); + from("direct:listOrders").to("mock:listOrders"); + from("direct:createOrder").to("mock:createOrder"); + } + }; + } + + @Override + protected CamelContext createCamelContext(String componentName) { + camelContext = new DefaultCamelContext(); + PlatformHttpComponent httpCmpn = mock(PlatformHttpComponent.class); + camelContext.addComponent("platform-http", httpCmpn); + return camelContext; + } + + private RestOpenApiProcessor createProcessor() throws Exception { + OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "unmatched-request-handler.yaml"); + String basePath = RestOpenApiHelper.determineBasePath(camelContext, null, null, openApi); + + DefaultRestOpenapiProcessorStrategy strategy = new DefaultRestOpenapiProcessorStrategy(); + strategy.setCamelContext(camelContext); + + RestOpenApiComponent component = new RestOpenApiComponent(); + RestOpenApiEndpoint endpoint = new RestOpenApiEndpoint( + "rest-openapi:unmatched-request-handler.yaml", "unmatched-request-handler.yaml", component, null); + + RestOpenApiProcessor processor = new RestOpenApiProcessor(endpoint, openApi, basePath, null, strategy); + processor.setCamelContext(camelContext); + processor.setPlatformHttpConsumer(createMockPlatformHttpConsumerAware()); + processor.afterPropertiesConfigured(camelContext); + openApiProcessor = processor; + return processor; + } + + private PlatformHttpConsumerAware createMockPlatformHttpConsumerAware() { + PlatformHttpConsumerAware platformHttpConsumerAware = mock(PlatformHttpConsumerAware.class); + PlatformHttpConsumer platformHttpConsumer = mock(PlatformHttpConsumer.class); + PlatformHttpEndpoint platformHttpEndpoint = mock(PlatformHttpEndpoint.class); + when(platformHttpConsumerAware.getPlatformHttpConsumer()).thenReturn(platformHttpConsumer); + when(platformHttpConsumer.getEndpoint()).thenReturn(platformHttpEndpoint); + when(platformHttpEndpoint.getServiceUrl()).thenReturn("http://localhost:8080"); + return platformHttpConsumerAware; + } + + private Exchange send(RestOpenApiProcessor processor, String path, String verb) throws Exception { + Exchange exchange = new DefaultExchange(camelContext); + exchange.getMessage().setHeader(Exchange.HTTP_PATH, path); + exchange.getMessage().setHeader(Exchange.HTTP_METHOD, verb); + processor.process(exchange, done -> { + }); + return exchange; + } + + @Test + void testDefaultHandlerReturns404WithEmptyBody() throws Exception { + RestOpenApiProcessor processor = createProcessor(); + Exchange exchange = send(processor, "/unknown", "GET"); + + assertEquals(404, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + assertNull(exchange.getMessage().getBody()); + assertTrue(exchange.isRouteStop()); + } + + @Test + void testDefaultHandlerReturns405WithAllowHeader() throws Exception { + RestOpenApiProcessor processor = createProcessor(); + Exchange exchange = send(processor, "/orders", "PUT"); + + assertEquals(405, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + assertEquals("GET, POST", exchange.getMessage().getHeader("Allow", String.class)); + assertNull(exchange.getMessage().getBody()); + assertTrue(exchange.isRouteStop()); + } + + @Test + void testCustomHandlerFromRegistryIsCalled() throws Exception { + RecordingHandler handler = new RecordingHandler(); + camelContext.getRegistry().bind("customHandler", handler); + + RestOpenApiProcessor processor = createProcessor(); + Exchange exchange = send(processor, "/unknown", "GET"); + + assertEquals(404, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + assertEquals("{\"error\":\"not found\"}", exchange.getMessage().getBody(String.class)); + assertEquals(List.of(404), handler.statusCodes); + } + + @Test + void testCustomHandlerReceivesCorrectStatusCode() throws Exception { + RecordingHandler handler = new RecordingHandler(); + camelContext.getRegistry().bind("customHandler", handler); + + RestOpenApiProcessor processor = createProcessor(); + + Exchange notFound = send(processor, "/unknown", "GET"); + Exchange methodNotAllowed = send(processor, "/orders", "PUT"); + + assertEquals(List.of(404, 405), handler.statusCodes); + assertEquals(404, notFound.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + assertEquals(405, methodNotAllowed.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + } + + @Test + void testCustomHandlerReceivesCorrectAllowedMethods() throws Exception { + RecordingHandler handler = new RecordingHandler(); + camelContext.getRegistry().bind("customHandler", handler); + + RestOpenApiProcessor processor = createProcessor(); + + send(processor, "/unknown", "GET"); + send(processor, "/orders", "PUT"); + + assertEquals(List.of(List.of(), List.of("GET", "POST")), handler.allowedMethods); + } + + @AfterEach + void stopProcessor() throws Exception { + if (openApiProcessor != null) { + ServiceHelper.stopService(openApiProcessor); + openApiProcessor = null; + } + } + + static final class RecordingHandler implements RestOpenApiUnmatchedRequestHandler { + + final List statusCodes = new ArrayList<>(); + final List> allowedMethods = new ArrayList<>(); + + @Override + public void handle(Exchange exchange, int statusCode, List allowedMethods) { + statusCodes.add(statusCode); + this.allowedMethods.add(List.copyOf(allowedMethods)); + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); + if (!allowedMethods.isEmpty()) { + exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods)); + } + exchange.getMessage().setBody("{\"error\":\"not found\"}"); + } + } +} diff --git a/components/camel-rest-openapi/src/test/resources/unmatched-request-handler.yaml b/components/camel-rest-openapi/src/test/resources/unmatched-request-handler.yaml new file mode 100644 index 0000000000000..7466d90d4fca2 --- /dev/null +++ b/components/camel-rest-openapi/src/test/resources/unmatched-request-handler.yaml @@ -0,0 +1,42 @@ +# +# 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. +# + +openapi: 3.0.0 +info: + title: Unmatched request handler test API + version: 1.0.0 +paths: + /users: + get: + summary: Returns a list of users + operationId: listUsers + responses: + '200': + description: A list of users + /orders: + get: + summary: Returns a list of orders + operationId: listOrders + responses: + '200': + description: A list of orders + post: + summary: Creates an order + operationId: createOrder + responses: + '201': + description: Order created From beac47ca24514e775ff51da13489e2a234e8ec64 Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Thu, 10 Sep 2026 09:13:34 +0100 Subject: [PATCH 2/6] Fix for unmatchedRequestHandler assignment issue --- .../camel/component/rest/openapi/RestOpenApiProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e26b3319f2816..8c89be54014a5 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 @@ -50,7 +50,7 @@ public class RestOpenApiProcessor extends AsyncProcessorSupport implements Camel private final String apiContextPath; private final List> paths = new ArrayList<>(); private final RestOpenapiProcessorStrategy restOpenapiProcessorStrategy; - private RestOpenApiUnmatchedRequestHandler unmatchedRequestHandler; + private RestOpenApiUnmatchedRequestHandler unmatchedRequestHandler = new DefaultRestOpenApiUnmatchedRequestHandler(); private PlatformHttpConsumerAware platformHttpConsumer; private Consumer consumer; private OpenApiUtils openApiUtils; From 2b21b626f8924587602a6ff89060e02f3d9761ec Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Thu, 10 Sep 2026 09:14:18 +0100 Subject: [PATCH 3/6] Documenting the factory finder route to register handler --- .../apache/camel/catalog/docs/rest-openapi-component.adoc | 7 +++++++ .../src/main/docs/rest-openapi-component.adoc | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc index 7fd566da64be8..1054937dabee5 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc @@ -210,6 +210,13 @@ The handler is called with the exchange, the status code (`404` or `405`) and th methods (empty for `404`), and can then set the response body, status code and headers as needed. When no handler is registered, the default (empty body) response is used. +The handler can also be registered using a factory finder on the classpath. This is done by adding a +resource file `META-INF/services/org/apache/camel/rest-openapi-unmatched-request-handler-factory` +with the content `class=com.example.MyHandler`. + +Regardless of how the handler is registered *only one* handler is supported: if two or more beans of this type +are found in the registry, the default handler is used. + [source,java] ---- public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler { diff --git a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc index 7fd566da64be8..1054937dabee5 100644 --- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc +++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc @@ -210,6 +210,13 @@ The handler is called with the exchange, the status code (`404` or `405`) and th methods (empty for `404`), and can then set the response body, status code and headers as needed. When no handler is registered, the default (empty body) response is used. +The handler can also be registered using a factory finder on the classpath. This is done by adding a +resource file `META-INF/services/org/apache/camel/rest-openapi-unmatched-request-handler-factory` +with the content `class=com.example.MyHandler`. + +Regardless of how the handler is registered *only one* handler is supported: if two or more beans of this type +are found in the registry, the default handler is used. + [source,java] ---- public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler { From 34001d010c1ab77a333f6a32ff714b33e7841497 Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Thu, 10 Sep 2026 09:19:22 +0100 Subject: [PATCH 4/6] Add test to cover factory finder --- ...estOpenApiUnmatchedRequestHandlerTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java index c45a9c9a33aa4..0b68ed09625d3 100644 --- a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java +++ b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java @@ -16,8 +16,11 @@ */ package org.apache.camel.component.rest.openapi; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import io.swagger.v3.oas.models.OpenAPI; import org.apache.camel.CamelContext; @@ -29,6 +32,9 @@ import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer; import org.apache.camel.component.platform.http.spi.PlatformHttpConsumerAware; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.impl.engine.DefaultFactoryFinder; +import org.apache.camel.spi.ClassResolver; +import org.apache.camel.spi.FactoryFinder; import org.apache.camel.support.DefaultExchange; import org.apache.camel.support.service.ServiceHelper; import org.junit.jupiter.api.AfterEach; @@ -171,6 +177,37 @@ void testCustomHandlerReceivesCorrectAllowedMethods() throws Exception { assertEquals(List.of(List.of(), List.of("GET", "POST")), handler.allowedMethods); } + @Test + void testCustomHandlerFromFactoryFinderIsCalled() throws Exception { + // Since we want to be ablet to test both a bean regitered directly into + // the registry and the factory finder we can not just put the factory + // file into src/test/resources/META-INF/services that breaks other tests + ClassResolver classResolver = mock(ClassResolver.class); + String properties = "class=" + FactoryFoundHandler.class.getName(); + when(classResolver.loadResourceAsStream( + FactoryFinder.DEFAULT_PATH + RestOpenApiUnmatchedRequestHandler.FACTORY)) + .thenAnswer(invocation -> new ByteArrayInputStream(properties.getBytes(StandardCharsets.UTF_8))); + when(classResolver.resolveClass(FactoryFoundHandler.class.getName())) + .thenAnswer(invocation -> FactoryFoundHandler.class); + + FactoryFinder realFinder = camelContext.getCamelContextExtension().getBootstrapFactoryFinder(); + FactoryFinder factoryFinder = new DefaultFactoryFinder(classResolver, FactoryFinder.DEFAULT_PATH) { + @Override + public Optional> findOptionalClass(String key) { + return RestOpenApiUnmatchedRequestHandler.FACTORY.equals(key) + ? super.findOptionalClass(key) + : realFinder.findOptionalClass(key); + } + }; + camelContext.getCamelContextExtension().setBootstrapFactoryFinder(factoryFinder); + + RestOpenApiProcessor processor = createProcessor(); + Exchange exchange = send(processor, "/unknown", "GET"); + + assertEquals(404, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class)); + assertEquals("{\"error\":\"from factory finder\"}", exchange.getMessage().getBody(String.class)); + } + @AfterEach void stopProcessor() throws Exception { if (openApiProcessor != null) { @@ -195,4 +232,13 @@ public void handle(Exchange exchange, int statusCode, List allowedMethod exchange.getMessage().setBody("{\"error\":\"not found\"}"); } } + + public static final class FactoryFoundHandler implements RestOpenApiUnmatchedRequestHandler { + + @Override + public void handle(Exchange exchange, int statusCode, List allowedMethods) { + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); + exchange.getMessage().setBody("{\"error\":\"from factory finder\"}"); + } + } } From 472d4696bd63f3aeb7058bde95f0fa70436509e6 Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Wed, 16 Sep 2026 12:48:37 +0100 Subject: [PATCH 5/6] Clarify precedence of handlers in docs --- .../apache/camel/catalog/docs/rest-openapi-component.adoc | 6 ++++-- .../src/main/docs/rest-openapi-component.adoc | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc index 1054937dabee5..65b6bdc6c259f 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc @@ -214,8 +214,10 @@ The handler can also be registered using a factory finder on the classpath. This resource file `META-INF/services/org/apache/camel/rest-openapi-unmatched-request-handler-factory` with the content `class=com.example.MyHandler`. -Regardless of how the handler is registered *only one* handler is supported: if two or more beans of this type -are found in the registry, the default handler is used. +A single handler bean in the registry takes precedence over a handler found via the factory finder, which in +turn takes precedence over the default handler. Regardless of how the handler is registered *only one* handler +is used: when two or more beans of this type are found in the registry, none of them is used, instead the +handler from the factory finder is used instead (if present), otherwise the default handler is used. [source,java] ---- diff --git a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc index 1054937dabee5..65b6bdc6c259f 100644 --- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc +++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc @@ -214,8 +214,10 @@ The handler can also be registered using a factory finder on the classpath. This resource file `META-INF/services/org/apache/camel/rest-openapi-unmatched-request-handler-factory` with the content `class=com.example.MyHandler`. -Regardless of how the handler is registered *only one* handler is supported: if two or more beans of this type -are found in the registry, the default handler is used. +A single handler bean in the registry takes precedence over a handler found via the factory finder, which in +turn takes precedence over the default handler. Regardless of how the handler is registered *only one* handler +is used: when two or more beans of this type are found in the registry, none of them is used, instead the +handler from the factory finder is used instead (if present), otherwise the default handler is used. [source,java] ---- From 62c25f06c8c242d25540bb6a9b5f512d86e39fab Mon Sep 17 00:00:00 2001 From: Pontus Ullgren Date: Wed, 16 Sep 2026 12:58:34 +0100 Subject: [PATCH 6/6] Remove payload before response in default handler --- .../openapi/DefaultRestOpenApiUnmatchedRequestHandler.java | 3 ++- .../rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java index 93cceabae99a6..08b9db8fe759f 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenApiUnmatchedRequestHandler.java @@ -22,7 +22,7 @@ /** * Default {@link RestOpenApiUnmatchedRequestHandler} that returns empty body with the HTTP status code and, if - * provided, the {@code Allow} header. + * provided, the {@code Allow} header. The payload in the incoming request message is removed. * * @since 4.23 */ @@ -30,6 +30,7 @@ public class DefaultRestOpenApiUnmatchedRequestHandler implements RestOpenApiUnm @Override public void handle(Exchange exchange, int statusCode, List allowedMethods) { + exchange.getMessage().setBody(null); exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode); if (!allowedMethods.isEmpty()) { exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods)); diff --git a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java index 0b68ed09625d3..808845430e0d1 100644 --- a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java +++ b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiUnmatchedRequestHandlerTest.java @@ -110,6 +110,7 @@ private Exchange send(RestOpenApiProcessor processor, String path, String verb) Exchange exchange = new DefaultExchange(camelContext); exchange.getMessage().setHeader(Exchange.HTTP_PATH, path); exchange.getMessage().setHeader(Exchange.HTTP_METHOD, verb); + exchange.getMessage().setBody("request-payload"); processor.process(exchange, done -> { }); return exchange;