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

Reply via email to