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 51cfc77abea1 CAMEL-25321: camel-rest-openapi - client request
validation checks the path parameters too (#27333)
51cfc77abea1 is described below
commit 51cfc77abea19e1a2cba340da5627c2b92766daa
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 4 15:39:25 2026 +0200
CAMEL-25321: camel-rest-openapi - client request validation checks the path
parameters too (#27333)
With requestValidationEnabled the producer validated the content type, a
required body, JSON syntax, required query and header parameters, but not
the path parameters: a call without a value for {sku} passed and went out
with the literal {sku} in the path. Every path parameter is required in
OpenAPI, so each placeholder of the operation's path template (which covers
a parameter declared on the path item too) now needs a value from a header,
an exchange variable or an endpoint parameter, else: "Path parameter 'sku'
is required but none found: set the header sku, or an exchange variable of
that name, before the call."
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/catalog/docs/rest-openapi-component.adoc | 4 ++
.../src/main/docs/rest-openapi-component.adoc | 4 ++
.../openapi/validator/DefaultRequestValidator.java | 32 +++++++++++
.../openapi/RestOpenApiRequestValidationTest.java | 63 ++++++++++++++++++++++
4 files changed, 103 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 010f86b443a4..b7138eab3a7d 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
@@ -350,6 +350,10 @@ expected to be present among the Camel message exchange
headers.
* query parameters - Validates whether an HTTP query parameter required by the
API operation is present. The query parameter is
expected to be present among the Camel message exchange headers.
+* path parameters - Validates that every parameter in the path of the API
operation, such as `sku` in
+`/stock/\{sku}/reserve`, has a value: a Camel message exchange header or an
exchange variable of that name. Without
+the check, a missing value is sent as the literal `\{sku}` in the path.
+
If any of the validation checks fail, then a `RestOpenApiValidationException`
is thrown. The exception object
has a `getValidationErrors` method that returns the error messages from the
validator.
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 010f86b443a4..b7138eab3a7d 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
@@ -350,6 +350,10 @@ expected to be present among the Camel message exchange
headers.
* query parameters - Validates whether an HTTP query parameter required by the
API operation is present. The query parameter is
expected to be present among the Camel message exchange headers.
+* path parameters - Validates that every parameter in the path of the API
operation, such as `sku` in
+`/stock/\{sku}/reserve`, has a value: a Camel message exchange header or an
exchange variable of that name. Without
+the check, a missing value is sent as the literal `\{sku}` in the path.
+
If any of the validation checks fail, then a `RestOpenApiValidationException`
is thrown. The exception object
has a `getValidationErrors` method that returns the error messages from the
validator.
diff --git
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
index 112b951da96d..89c42ea061c5 100644
---
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
+++
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/DefaultRequestValidator.java
@@ -21,6 +21,8 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.json.JsonMapper;
@@ -34,6 +36,8 @@ import static
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentTyp
public class DefaultRequestValidator implements RequestValidator {
+ private static final Pattern PATH_PARAMETER =
Pattern.compile("\\{([^}/]+)}");
+
private RestOpenApiOperation operation;
private Map<String, Object> endpointParameters = Collections.emptyMap();
@@ -119,6 +123,23 @@ public class DefaultRequestValidator implements
RequestValidator {
}
});
+ // Validate path parameters: every placeholder of the path is required
(OpenAPI), and one without a value would
+ // be sent as the literal {name}. Taken from the path template, so a
parameter declared on the path item counts
+ // too (CAMEL-25321)
+ for (String name : pathParameters(o.getUriTemplate())) {
+ Object value = message.getHeader(name);
+ if (ObjectHelper.isEmpty(value)) {
+ value = exchange.getVariable(name);
+ }
+ if (ObjectHelper.isEmpty(value)) {
+ value = endpointParameters.get(name);
+ }
+ if (ObjectHelper.isEmpty(value)) {
+ validationErrors.add("Path parameter '" + name + "' is
required but none found: set the header " + name
+ + ", or an exchange variable of that
name, before the call.");
+ }
+ }
+
// Validate operation required headers
o.getHeaders()
.stream()
@@ -136,4 +157,15 @@ public class DefaultRequestValidator implements
RequestValidator {
return Collections.unmodifiableSet(validationErrors);
}
+ /** The names of the placeholders of a path template: {@code
/stock/{sku}/reserve} gives {@code sku}. */
+ static Set<String> pathParameters(String uriTemplate) {
+ Set<String> names = new LinkedHashSet<>();
+ if (uriTemplate != null) {
+ Matcher m = PATH_PARAMETER.matcher(uriTemplate);
+ while (m.find()) {
+ names.add(m.group(1));
+ }
+ }
+ return names;
+ }
}
diff --git
a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiRequestValidationTest.java
b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiRequestValidationTest.java
index 3a4a2b951c79..23b75c789fb1 100644
---
a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiRequestValidationTest.java
+++
b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiRequestValidationTest.java
@@ -296,6 +296,47 @@ public class RestOpenApiRequestValidationTest extends
CamelTestSupport {
assertEquals("Pet deleted", result);
}
+ @ParameterizedTest
+ @MethodSource("petStoreVersions")
+ void requestValidationWithMissingPathParameter(String petStoreVersion) {
+ // no petId: without the check the request went out as /pet/{petId}
(CAMEL-25321)
+ Exchange exchange = template.request("direct:validateDelete", new
Processor() {
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ exchange.getMessage().setHeader("petStoreVersion",
petStoreVersion);
+ exchange.getMessage().setHeader("api_key", "foo");
+ }
+ });
+
+ Exception exception = exchange.getException();
+ assertNotNull(exception);
+ assertInstanceOf(RestOpenApiValidationException.class, exception);
+ Set<String> errors = ((RestOpenApiValidationException)
exception).getValidationErrors();
+ assertEquals(1, errors.size());
+ assertEquals("Path parameter 'petId' is required but none found: set
the header petId, or an exchange variable "
+ + "of that name, before the call.",
+ errors.iterator().next());
+ }
+
+ @ParameterizedTest
+ @MethodSource("petStoreVersions")
+ void requestValidationWithPathParameterFromVariable(String
petStoreVersion) {
+ Exchange exchange = template.request("direct:validateDelete", new
Processor() {
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ exchange.getMessage().setHeader("petStoreVersion",
petStoreVersion);
+ exchange.getMessage().setHeader("api_key", "foo");
+ exchange.setVariable("petId", 10);
+ }
+ });
+
+ Exception exception = exchange.getException();
+ if (exception != null) {
+ throw new AssertionError("Unexpected validation failure",
exception);
+ }
+ assertEquals("Pet deleted",
exchange.getMessage().getBody(String.class));
+ }
+
@ParameterizedTest
@MethodSource("petStoreVersions")
@SuppressWarnings("unchecked")
@@ -364,6 +405,8 @@ public class RestOpenApiRequestValidationTest extends
CamelTestSupport {
@Override
public void process(Exchange exchange) throws Exception {
exchange.getMessage().setHeader("fruitsApiVersion",
fruitsApiVersion);
+ // the path parameter is given, so only the missing header
fails the validation
+ exchange.getMessage().setHeader("id", 1);
}
});
@@ -377,6 +420,26 @@ public class RestOpenApiRequestValidationTest extends
CamelTestSupport {
assertTrue(errors.iterator().next().startsWith("Header parameter
'deletionReason' is required"));
}
+ @ParameterizedTest
+ @MethodSource("fruitsApiVersions")
+ void requestValidationReportsEveryMissingParameter(String
fruitsApiVersion) {
+ // neither the path parameter id nor the required header
deletionReason: both are reported, not only the first
+ Exchange exchange = template.request("direct:headerParam", new
Processor() {
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ exchange.getMessage().setHeader("fruitsApiVersion",
fruitsApiVersion);
+ }
+ });
+
+ Exception exception = exchange.getException();
+ assertInstanceOf(RestOpenApiValidationException.class, exception);
+ Set<String> errors = ((RestOpenApiValidationException)
exception).getValidationErrors();
+ assertEquals(2, errors.size());
+ assertTrue(errors.stream().anyMatch(e -> e.startsWith("Path parameter
'id' is required")), errors.toString());
+ assertTrue(errors.stream().anyMatch(e -> e.startsWith("Header
parameter 'deletionReason' is required")),
+ errors.toString());
+ }
+
@ParameterizedTest
@MethodSource("fruitsApiVersions")
void requestValidationRequiredHeaderParamsPresent(String fruitsApiVersion)
{