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 d3ed374d051c CAMEL-25187: camel-support - accept media ranges such as
application/* in the Accept header (#27315)
d3ed374d051c is described below
commit d3ed374d051cbd6b8d29d5e38273751a6f262f05
Author: Chakravarthy Batna
<[email protected]>
AuthorDate: Sun Oct 4 19:59:06 2026 +0530
CAMEL-25187: camel-support - accept media ranges such as application/* in
the Accept header (#27315)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../component/jetty/rest/RestJettyAcceptTest.java | 24 ++++++++++++
.../jetty/rest/RestJettyContentTypeTest.java | 13 +++++++
.../rest/openapi/RestOpenApiProcessor.java | 44 ++--------------------
.../openapi/RestUnmatchedRequestHandlerTest.java | 24 ------------
.../apache/camel/support/http/RestUtilTest.java | 38 +++++++++++++++++++
.../org/apache/camel/support/http/RestUtil.java | 35 ++++++++++++++++-
.../DefaultRestClientRequestValidator.java | 3 +-
7 files changed, 114 insertions(+), 67 deletions(-)
diff --git
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyAcceptTest.java
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyAcceptTest.java
index e19c531923fe..8da769e219ef 100644
---
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyAcceptTest.java
+++
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyAcceptTest.java
@@ -69,6 +69,30 @@ public class RestJettyAcceptTest extends BaseJettyTest {
assertEquals("", cause.getResponseBody());
}
+ @Test
+ public void testJettyProducerAcceptMediaRangeValid() {
+ String out = fluentTemplate.withHeader(Exchange.CONTENT_TYPE,
"application/json")
+ .withHeader("Accept",
"application/*").withHeader(Exchange.HTTP_METHOD, "post")
+ .withBody("{ \"name\": \"Donald Duck\"
}").to("http://localhost:" + getPort() + "/users/123/update")
+ .request(String.class);
+
+ assertEquals("{ \"status\": \"ok\" }", out);
+ }
+
+ @Test
+ public void testJettyProducerAcceptMediaRangeInvalid() {
+ FluentProducerTemplate requestTemplate =
fluentTemplate.withHeader(Exchange.CONTENT_TYPE, "application/json")
+ .withHeader("Accept", "text/*")
+ .withHeader(Exchange.HTTP_METHOD, "post")
+ .withBody("{ \"name\": \"Donald Duck\" }")
+ .to("http://localhost:" + getPort() + "/users/123/update");
+
+ Exception ex = assertThrows(CamelExecutionException.class, () ->
requestTemplate.request(String.class));
+
+ HttpOperationFailedException cause =
assertIsInstanceOf(HttpOperationFailedException.class, ex.getCause());
+ assertEquals(406, cause.getStatusCode());
+ }
+
@Test
public void testGetContentTypeHeaderOk() throws Exception {
// use JDK client as camel-http will drop "Content-Type" header for GET
diff --git
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyContentTypeTest.java
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyContentTypeTest.java
index beb46a12884b..ab944413f4be 100644
---
a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyContentTypeTest.java
+++
b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/rest/RestJettyContentTypeTest.java
@@ -62,6 +62,19 @@ public class RestJettyContentTypeTest extends BaseJettyTest {
assertEquals("", cause.getResponseBody());
}
+ @Test
+ public void testJettyProducerContentTypeMediaRangeInvalid() {
+ FluentProducerTemplate requestTemplate =
fluentTemplate.withHeader(Exchange.CONTENT_TYPE, "application/*")
+ .withHeader(Exchange.HTTP_METHOD, "post")
+ .withBody("{ \"name\": \"Donald Duck\" }")
+ .to("http://localhost:" + getPort() + "/users/123/update");
+
+ Exception ex = assertThrows(CamelExecutionException.class, () ->
requestTemplate.request(String.class));
+
+ HttpOperationFailedException cause =
assertIsInstanceOf(HttpOperationFailedException.class, ex.getCause());
+ assertEquals(415, cause.getStatusCode());
+ }
+
@Test
public void testJettyMultiProducerContentTypeValid() {
String out = fluentTemplate.withHeader("Accept", "application/csv")
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 509e176e111c..124112fb60a9 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.Locale;
import java.util.Optional;
import io.swagger.v3.oas.models.OpenAPI;
@@ -36,11 +35,11 @@ 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.http.RestUtil;
import org.apache.camel.support.processor.RestBindingAdvice;
import org.apache.camel.support.processor.RestBindingAdviceFactory;
import org.apache.camel.support.processor.RestBindingConfiguration;
import org.apache.camel.support.service.ServiceHelper;
-import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -122,11 +121,11 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
// Camel and must be answered here instead of being processed as
if they were valid
if (serverRequestValidation && unmatchedRequestCatchAllRegistered)
{
String contentType =
exchange.getMessage().getHeader(Exchange.CONTENT_TYPE, String.class);
- if (!isValidOrAcceptedContentType(rcp.getConsumes(),
contentType)) {
+ if (!RestUtil.isValidOrAcceptedContentType(rcp.getConsumes(),
contentType)) {
return answerUnmatchedRequest(exchange, callback, 415,
List.of());
}
String accept = exchange.getMessage().getHeader("Accept",
String.class);
- if (!isValidOrAcceptedContentType(rcp.getProduces(), accept)) {
+ if (!RestUtil.isAcceptedMediaType(rcp.getProduces(), accept)) {
return answerUnmatchedRequest(exchange, callback, 406,
List.of());
}
}
@@ -178,43 +177,6 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
return true;
}
- /**
- * Whether the given Content-Type or Accept header value matches the
consumes/produces of the operation. Unlike
- * RestUtil#isValidOrAcceptedContentType, the parameters of each part are
stripped and subtype wildcards are
- * supported.
- */
- static boolean isValidOrAcceptedContentType(String valid, String target) {
- if (valid == null || target == null) {
- return true;
- }
-
- // Any MIME type
- //
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#Directives
- if (target.contains("*/*")) {
- return true;
- }
-
- valid = valid.toLowerCase(Locale.ENGLISH);
- target = target.toLowerCase(Locale.ENGLISH);
-
- // try each part of the target without its parameters
- for (String part : target.split(",")) {
- part = StringHelper.before(part, ";", part).trim();
- if (part.isEmpty()) {
- continue;
- }
- if (valid.contains(part)) {
- return true;
- }
- // subtype wildcard such as application/* matches any subtype of
that type
- if (part.endsWith("/*") && valid.contains(part.substring(0,
part.length() - 1))) {
- return true;
- }
- }
-
- return false;
- }
-
@Override
protected void doInit() throws Exception {
super.doInit();
diff --git
a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestUnmatchedRequestHandlerTest.java
b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestUnmatchedRequestHandlerTest.java
index fa774575ea99..22d563008afd 100644
---
a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestUnmatchedRequestHandlerTest.java
+++
b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestUnmatchedRequestHandlerTest.java
@@ -311,30 +311,6 @@ class RestUnmatchedRequestHandlerTest extends
ManagedCamelTestSupport {
assertEquals(List.of(), handler.statusCodes);
}
- @Test
- void testIsValidOrAcceptedContentType() {
- assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType(null,
null));
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
null));
- assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType(null,
"application/json"));
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"*/*"));
-
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"application/json"));
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/xml",
"application/json,application/xml"));
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"APPLICATION/JSON"));
-
assertFalse(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"application/xml"));
-
- assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType(
- "application/json", "application/xml;q=0.9,
application/json"));
- assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType(
- "application/json", "application/json;q=0.9,
application/xml;q=0.8"));
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"application/json; charset=UTF-8"));
- assertFalse(RestOpenApiProcessor.isValidOrAcceptedContentType(
- "application/json", "application/xml;q=0.9, text/plain"));
-
-
assertTrue(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"application/*"));
-
assertFalse(RestOpenApiProcessor.isValidOrAcceptedContentType("application/json",
"text/*"));
- }
-
@Test
void testCustomHandlerFromFactoryFinderIsCalled() throws Exception {
// Since we want to be able to test both a bean registered directly
into
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
index 787fe04f8a3f..5976a2dabbe7 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
@@ -24,11 +24,14 @@ public class RestUtilTest {
@Test
public void testRestUtil() {
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType(null,
null));
+
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
null));
+ Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType(null,
"application/json"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType(null,
"*/*"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"*/*"));
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/json"));
+
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"APPLICATION/JSON"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/json,application/xml"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/json, application/xml"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml,application/json"));
@@ -53,9 +56,44 @@ public class RestUtilTest {
// the parameters of each media type (such as q or charset) are not
part of the match
Assertions.assertTrue(
RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml;q=0.9, application/json"));
+ Assertions.assertTrue(
+ RestUtil.isValidOrAcceptedContentType("application/json",
"application/json;q=0.9, application/xml;q=0.8"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/json; charset=UTF-8"));
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml;q=0.9, text/plain"));
// an empty part does not match
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml,"));
}
+
+ @Test
+ public void testAcceptedMediaType() {
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType(null, null));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
null));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"*/*"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"application/json"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"application/xml;q=0.9, application/json"));
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("application/json",
"application/xml"));
+
+ // a media range such as application/* accepts any subtype of its type
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"application/*"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"Application/*"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"application/*;q=0.8"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json",
"text/html, application/*;q=0.8"));
+
Assertions.assertTrue(RestUtil.isAcceptedMediaType("application/json,application/xml",
"application/*"));
+
Assertions.assertTrue(RestUtil.isAcceptedMediaType("text/plain,application/xml",
"application/*"));
+ Assertions.assertTrue(RestUtil.isAcceptedMediaType("text/plain,
application/xml", "application/*"));
+
+ // the type of the media range must be the same type, not only start
or end the same
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("application/json",
"text/*"));
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("application/json",
"text/*, image/*"));
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("application/json",
"app/*"));
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("xapplication/json",
"application/*"));
+
Assertions.assertFalse(RestUtil.isAcceptedMediaType("application/json", "/*"));
+ }
+
+ @Test
+ public void testContentTypeWithMediaRange() {
+ // a media range is only valid in the Accept header, not as a
Content-Type
+
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/*"));
+
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/*; charset=UTF-8"));
+ }
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
b/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
index 6ae69a5fb452..d13a58466800 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
@@ -27,6 +27,19 @@ public final class RestUtil {
* Accept/Content-Type headers.
*/
public static boolean isValidOrAcceptedContentType(String valid, String
target) {
+ return isValidOrAccepted(valid, target, false);
+ }
+
+ /**
+ * Used for validating the Accept header of incoming REST calls against
produces. Unlike
+ * {@link #isValidOrAcceptedContentType(String, String)} a media range
such as application/* is supported, as it is
+ * only valid in the Accept header.
+ */
+ public static boolean isAcceptedMediaType(String produces, String accept) {
+ return isValidOrAccepted(produces, accept, true);
+ }
+
+ private static boolean isValidOrAccepted(String valid, String target,
boolean mediaRange) {
if (valid == null || target == null) {
return true;
}
@@ -44,7 +57,7 @@ public final class RestUtil {
for (String part : target.split(",")) {
// the media type is before its optional parameters (such as
charset or q)
part = StringHelper.before(part, ";", part).trim();
- if (!part.isEmpty() && valid.contains(part)) {
+ if (!part.isEmpty() && (valid.contains(part) || mediaRange &&
isAcceptedByMediaRange(valid, part))) {
return true;
}
}
@@ -52,4 +65,24 @@ public final class RestUtil {
return false;
}
+ /**
+ * Whether the part is a media range (such as application/*) that accepts
one of the valid media types, which is any
+ * media type of the same type.
+ */
+ private static boolean isAcceptedByMediaRange(String valid, String part) {
+ if (!part.endsWith("/*")) {
+ return false;
+ }
+ String type = part.substring(0, part.length() - 2);
+ if (type.isEmpty()) {
+ return false;
+ }
+ for (String mediaType : valid.split(",")) {
+ if (type.equals(StringHelper.before(mediaType.trim(), "/"))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
index af9ca20a755e..97ea611dee88 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultRestClientRequestValidator.java
@@ -26,6 +26,7 @@ import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.Jsoner;
+import static org.apache.camel.support.http.RestUtil.isAcceptedMediaType;
import static
org.apache.camel.support.http.RestUtil.isValidOrAcceptedContentType;
public class DefaultRestClientRequestValidator implements
RestClientRequestValidator {
@@ -40,7 +41,7 @@ public class DefaultRestClientRequestValidator implements
RestClientRequestValid
}
// check if what is produces is accepted by the client
String accept = exchange.getMessage().getHeader("Accept",
String.class);
- if (!isValidOrAcceptedContentType(validationContext.produces(),
accept)) {
+ if (!isAcceptedMediaType(validationContext.produces(), accept)) {
return new ValidationError(406, null);
}
// check for required query parameters