Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,49 @@ 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.

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`.

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]
----
public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler {

@Override
public void handle(Exchange exchange, int statusCode, List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,49 @@ 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
Comment thread
ullgren marked this conversation as resolved.
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.

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`.

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]
----
public class JsonErrorHandler implements RestOpenApiUnmatchedRequestHandler {

@Override
public void handle(Exchange exchange, int statusCode, List<String> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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. The payload in the incoming request message is removed.
*
* @since 4.23
*/
public class DefaultRestOpenApiUnmatchedRequestHandler implements RestOpenApiUnmatchedRequestHandler {

@Override
public void handle(Exchange exchange, int statusCode, List<String> allowedMethods) {
exchange.getMessage().setBody(null);
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode);
if (!allowedMethods.isEmpty()) {
exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -47,6 +50,7 @@ public class RestOpenApiProcessor extends AsyncProcessorSupport implements Camel
private final String apiContextPath;
private final List<RestConsumerContextPathMatcher.ConsumerPath<Operation>> paths = new ArrayList<>();
private final RestOpenapiProcessorStrategy restOpenapiProcessorStrategy;
private RestOpenApiUnmatchedRequestHandler unmatchedRequestHandler = new DefaultRestOpenApiUnmatchedRequestHandler();
private PlatformHttpConsumerAware platformHttpConsumer;
private Consumer consumer;
private OpenApiUtils openApiUtils;
Expand Down Expand Up @@ -130,13 +134,8 @@ public boolean process(Exchange exchange, AsyncCallback callback) {
final String contextPath = path;
List<String> 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;
Expand All @@ -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
Expand Down Expand Up @@ -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<RestOpenApiUnmatchedRequestHandler> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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";
Comment thread
ullgren marked this conversation as resolved.

/**
* 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<String> allowedMethods);
}
Loading