Hello, For one of our projects, we offer a set of REST/JSON APIs using Camel and wanted to activate the clientRequestValidation feature on those endpoints for Camel to reject invalid requests. But we encountered some limitations due to the way RestBindingAdvice handles the Accept header when the client specifies either multiple MIME types or JSON subtypes.
The current implementation includes some code to match JSON or XML subtypes (using String.contains()) but it is buggy and rejects all cases (no return true). All theses cases are described in the unit test below that includes 3 implementations of RestBindingAdvice.isValidOrAcceptedContentType(): - "Legacy" : isLegacyValidOrAcceptedContentType() = Camel 4.4.1 implementation - "Patched Legacy" : isPatchedLegacyValidOrAcceptedContentType() = a simple patch to fix for the "contains("xml/json") tests but with side-effects due to the whole Accept header not being parsed to process each MIME type separately - "Proposed" : isProposedValidOrAcceptedContentType() = an alternative implementation that parses the Accept header to test separately each of the MIME types specified by the client The "Proposed" implementation may be overkill for the Content-Type header as this header contains a single MIME type. (This is my first post, I do not know of the mailing list accepts mail attachments => code in the mail body below) Thank you, Laurent >>>>> import java.util.Arrays; import java.util.List; import java.util.Locale; import org.apache.camel.util.StringHelper; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class RestBindingAdvicePatchProposalTest { public final static String JSON = "application/json"; public final static String XML = "application/xml"; public final static String SPARQL_RESULT_JSON = "application/sparql-results+json"; public final static String JSON_LD = "application/ld+json"; @Test void testLegacyHeaderMatching() { // Supported cases (no patch) // 1. Exact match (single target type) assertTrue(isLegacyValidOrAcceptedContentType(JSON, JSON)); assertFalse(isLegacyValidOrAcceptedContentType(XML, JSON)); // 2. Exact match (single target type with parameter) assertTrue(isLegacyValidOrAcceptedContentType(JSON, JSON + ";charset=utf-8")); // 3. Wildcard match but supports for spec w/ multiple type = side-effect? assertTrue(isLegacyValidOrAcceptedContentType(JSON, XML + ", */*;q=0.5, text/plain")); // 4. But invalid "*/*foo" is also accepted => should not assertTrue(isLegacyValidOrAcceptedContentType(JSON, XML + ", */*foo;q=0.5, text/plain")); // Wildcard match: expected to work but does not assertFalse(isLegacyValidOrAcceptedContentType(JSON, XML + ", */json;q=0.5, text/plain")); assertFalse(isLegacyValidOrAcceptedContentType(JSON, JSON_LD)); // With patch: match any JSON-related type (spec w/ multiple types accepted but not fully parsed) assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML + ", */json;q=0.5, text/plain")); // But... // Invalid match: image/webjsonp => Accepted once patch accepts all "json" string assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML + ", image/webjsonp")); // JSON formatting of any type (e.g. application/xxx+json) is also accepted: OK ? assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, JSON_LD)); assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML + ", " + SPARQL_RESULT_JSON + ";charset=utf-8")); } @Test void testNewHeaderMatching() { // Supported cases, no regression // 1. Exact match (single target type) assertTrue(isProposedValidOrAcceptedContentType(JSON, JSON)); assertFalse(isProposedValidOrAcceptedContentType(XML, JSON)); // 2. Exact match (single target type with parameter) assertTrue(isProposedValidOrAcceptedContentType(JSON, JSON + ";charset=utf-8")); // 3. Wildcard match assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ", */*;q=0.5, text/plain")); // Cases now rejected // "*/*foo" shall be rejected assertFalse(isProposedValidOrAcceptedContentType(JSON, XML + ", */*foo;q=0.5, text/plain")); // "image/webjsonp" shall be rejected assertTrue(isPatchedLegacyValidOrAcceptedContentType(JSON, XML + ", image/webjsonp")); // JSON formatting of any type (e.g. application/xxx+json) no longer accepted: OK ? assertFalse(isProposedValidOrAcceptedContentType(JSON, JSON_LD)); assertFalse(isProposedValidOrAcceptedContentType(JSON, XML + ", " + SPARQL_RESULT_JSON + ";charset=utf-8")); // New (extended) behaviour // Exact match (multiple target types) assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ", " + JSON + ", text/plain")); assertTrue(isProposedValidOrAcceptedContentType(XML, "text/html, application/xhtml, " + XML + ";q=0.9")); // Wildcard match assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ", */json;q=0.5, text/plain")); assertTrue(isProposedValidOrAcceptedContentType(JSON, XML + ", application/*;q=0.5")); // Extended types assertTrue(isProposedValidOrAcceptedContentType(JSON_LD, JSON + ", application/*+json")); } private static boolean isProposedValidOrAcceptedContentType(String expected, String target) { if (expected == null || target == null) { return true; } String valid = expected.toLowerCase(Locale.ENGLISH); target = target.toLowerCase(Locale.ENGLISH); // Parse media range and strip any parameters (e.g. charset) for all found media types List<String> targetTypes = Arrays.stream(target.split("\\s*,\\s*")) .map(t -> StringHelper.before(t, ';', t)).toList(); // Search for exact match first if (targetTypes.contains(valid)) { return true; } else { // Match wildcard types ("*/*" or "application/*") or type extensions ("application/*+json") return targetTypes.stream().filter(t -> t.indexOf('*') != -1) .map(t -> t.replace("+", "\\+").replace("*", ".+")) .anyMatch(t -> valid.matches(t)); } } private static boolean isPatchedLegacyValidOrAcceptedContentType(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; } // content-type is before optional charset target = StringHelper.before(target, ";", target); valid = valid.toLowerCase(Locale.ENGLISH); target = target.toLowerCase(Locale.ENGLISH); if (valid.contains(target)) { return true; } // The 2 tests below are wrong: from this point on, method always returns false! boolean isXml = valid.contains("xml"); //if (isXml && !target.contains("xml")) { // return false; //} if (isXml && target.contains("xml")) { return true; } boolean isJson = valid.contains("json"); //if (isJson && !target.contains("json")) { // return false; //} if (isJson && target.contains("json")) { return true; } return false; } private static boolean isLegacyValidOrAcceptedContentType(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; } // content-type is before optional charset target = StringHelper.before(target, ";", target); valid = valid.toLowerCase(Locale.ENGLISH); target = target.toLowerCase(Locale.ENGLISH); if (valid.contains(target)) { return true; } boolean isXml = valid.contains("xml"); if (isXml && !target.contains("xml")) { return false; } boolean isJson = valid.contains("json"); if (isJson && !target.contains("json")) { return false; } return false; } } <<<<< Worldline, Cardlink, GoPay and Santeos are registered trademarks and trade names owned by the Worldline Group. This e-mail and any documents attached are confidential and intended solely for the addressee. It may also be privileged. If you are not the intended recipient of this e-mail, you are not authorized to copy, disclose, use or retain it. Please notify the sender immediately and delete this e-mail (including any attachments) from your systems. As e-mails may be intercepted, amended or lost, they are not secure. Therefore, Worldline’s and its subsidiaries’ liability cannot be triggered for the message content. Although the Worldline Group endeavors to maintain a virus-free network, we do not warrant that this e-mail is virus-free and do not accept liability for any damages, losses or consequences resulting from any transmitted virus if any. The risks are deemed to be accepted by anyone who communicates with Worldline or its subsidiaries by e-mail. Please consider the environment before printing, sending or forwarding this email.