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 0573d5df9aa6 CAMEL-24406: camel-xml-jaxp: add prolog guard to 
XmlConverter.toDOMDocument to avoid SAXParseException for non-XML content
0573d5df9aa6 is described below

commit 0573d5df9aa6cc10af508df4d6692ee5e33832f8
Author: mayurbm <[email protected]>
AuthorDate: Fri Aug 28 13:44:30 2026 +0530

    CAMEL-24406: camel-xml-jaxp: add prolog guard to XmlConverter.toDOMDocument 
to avoid SAXParseException for non-XML content
    
    XmlConverter.toDOMDocument(byte[], Exchange) and toDOMDocument(StreamCache,
    Exchange) passed content directly to DocumentBuilder.parse() without
    checking whether it could plausibly be XML, so non-XML payloads (empty
    body, JSON/HTML error responses, BOM-only) triggered an expensive
    SAXParseException deep inside the JDK parser.
    
    Add a cheap looksLikeXml(byte[]) helper that inspects the first bytes
    (handling UTF-8/UTF-16 BOMs and leading whitespace). When content cannot
    be XML, the two byte[]/StreamCache overloads now return null via
    @Converter(allowNull = true) instead of invoking DocumentBuilder.parse(),
    avoiding DOM allocation and GC pressure for large non-XML payloads. The
    generated CamelXmlJaxpBulkConverterLoader is regenerated to honour
    allowNull for these converters.
    
    Also fixes CxfPayloadConverter.tryConvertViaCxfPayload, which threw
    NPE/ClassCastException when the Document converter returned null (or
    Void.TYPE via the bulk loader) for non-XML content; it now falls through
    to the next conversion candidate.
    
    Adds XmlConverterPrologTest (unit + integration coverage) and two
    registry-path tests in XmlConverterTest.
    
    Closes #25555
    
    Co-authored-by: Claude <[email protected]>
---
 .../cxf/converter/CxfPayloadConverter.java         |   6 +-
 .../camel/converter/jaxp/XmlConverterTest.java     |  18 +++
 core/camel-xml-jaxp/pom.xml                        |   5 +
 .../jaxp/CamelXmlJaxpBulkConverterLoader.java      |  14 +-
 .../apache/camel/converter/jaxp/XmlConverter.java  |  61 ++++++++-
 .../converter/jaxp/XmlConverterPrologTest.java     | 149 +++++++++++++++++++++
 6 files changed, 244 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-cxf/camel-cxf-common/src/main/java/org/apache/camel/component/cxf/converter/CxfPayloadConverter.java
 
b/components/camel-cxf/camel-cxf-common/src/main/java/org/apache/camel/component/cxf/converter/CxfPayloadConverter.java
index 57a7b3ff2277..ab510270f27f 100644
--- 
a/components/camel-cxf/camel-cxf-common/src/main/java/org/apache/camel/component/cxf/converter/CxfPayloadConverter.java
+++ 
b/components/camel-cxf/camel-cxf-common/src/main/java/org/apache/camel/component/cxf/converter/CxfPayloadConverter.java
@@ -208,8 +208,10 @@ public final class CxfPayloadConverter {
         }
         final TypeConverter documentTc = registry.lookup(Document.class, 
value.getClass());
         if (documentTc != null) {
-            Document document = documentTc.convertTo(Document.class, exchange, 
value);
-            return (T) documentToCxfPayload(document, exchange);
+            Object result = documentTc.convertTo(Document.class, exchange, 
value);
+            if (result instanceof Document document) {
+                return (T) documentToCxfPayload(document, exchange);
+            }
         }
         // maybe we can convert via an InputStream
         final CxfPayload<?> inputStreamPayload = convertVia(InputStream.class, 
exchange, value, registry);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/converter/jaxp/XmlConverterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/converter/jaxp/XmlConverterTest.java
index 16ccef50bd4b..ecd6075c1872 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/converter/jaxp/XmlConverterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/converter/jaxp/XmlConverterTest.java
@@ -625,4 +625,22 @@ class XmlConverterTest extends ContextTestSupport {
         assertNotNull(node);
     }
 
+    @Test
+    void testToDOMDocumentReturnsNullForNonXmlByteArrayViaRegistry() {
+        // Exercises the bulk loader path (CamelXmlJaxpBulkConverterLoader) 
rather than
+        // calling XmlConverter.toDOMDocument() directly. When 
@Converter(allowNull=true)
+        // returns null, the loader returns Void.class, which the registry 
translates to null.
+        byte[] json = 
"{\"status\":\"error\"}".getBytes(java.nio.charset.StandardCharsets.UTF_8);
+        Document doc = context.getTypeConverter().convertTo(Document.class, 
json);
+        assertNull(doc, "Registry must return null (not throw) for non-XML 
byte[] via bulk loader");
+    }
+
+    @Test
+    void testToDOMDocumentParsesValidXmlByteArrayViaRegistry() throws 
Exception {
+        byte[] xml = 
"<root><child/></root>".getBytes(java.nio.charset.StandardCharsets.UTF_8);
+        Document doc = context.getTypeConverter().convertTo(Document.class, 
xml);
+        assertNotNull(doc, "Registry must parse valid XML byte[]");
+        assertEquals("root", doc.getDocumentElement().getTagName());
+    }
+
 }
diff --git a/core/camel-xml-jaxp/pom.xml b/core/camel-xml-jaxp/pom.xml
index 2cc1d68b625b..c15337b85771 100644
--- a/core/camel-xml-jaxp/pom.xml
+++ b/core/camel-xml-jaxp/pom.xml
@@ -58,6 +58,11 @@
         </dependency>
 
         <!-- testing -->
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.woodstox</groupId>
             <artifactId>woodstox-core</artifactId>
diff --git 
a/core/camel-xml-jaxp/src/generated/java/org/apache/camel/converter/jaxp/CamelXmlJaxpBulkConverterLoader.java
 
b/core/camel-xml-jaxp/src/generated/java/org/apache/camel/converter/jaxp/CamelXmlJaxpBulkConverterLoader.java
index 89845e2de230..f49b23d6e613 100644
--- 
a/core/camel-xml-jaxp/src/generated/java/org/apache/camel/converter/jaxp/CamelXmlJaxpBulkConverterLoader.java
+++ 
b/core/camel-xml-jaxp/src/generated/java/org/apache/camel/converter/jaxp/CamelXmlJaxpBulkConverterLoader.java
@@ -333,10 +333,20 @@ public final class CamelXmlJaxpBulkConverterLoader 
implements TypeConverterLoade
                 return getXmlConverter().toDOMDocument((org.w3c.dom.Node) 
value);
             }
             if (value instanceof byte[]) {
-                return getXmlConverter().toDOMDocument((byte[]) value, 
exchange);
+                Object obj = getXmlConverter().toDOMDocument((byte[]) value, 
exchange);
+                if (obj == null) {
+                    return Void.class;
+                } else {
+                    return obj;
+                }
             }
             if (value instanceof org.apache.camel.StreamCache) {
-                return 
getXmlConverter().toDOMDocument((org.apache.camel.StreamCache) value, exchange);
+                Object obj = 
getXmlConverter().toDOMDocument((org.apache.camel.StreamCache) value, exchange);
+                if (obj == null) {
+                    return Void.class;
+                } else {
+                    return obj;
+                }
             }
             if (value instanceof java.io.InputStream) {
                 return getXmlConverter().toDOMDocument((java.io.InputStream) 
value, exchange);
diff --git 
a/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java
 
b/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java
index d316a5eaf82a..fa1856973a60 100644
--- 
a/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java
+++ 
b/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java
@@ -691,20 +691,27 @@ public class XmlConverter {
      *
      * @param  data     is the data to be parsed
      * @param  exchange is the exchange to be used when calling the converter
-     * @return          the parsed document
+     * @return          the parsed document, or {@code null} if the byte 
content does not look like XML
      */
-    @Converter(order = 54)
+    @Converter(order = 54, allowNull = true)
     public Document toDOMDocument(byte[] data, Exchange exchange)
             throws IOException, SAXException, ParserConfigurationException {
+        if (!looksLikeXml(data)) {
+            return null;
+        }
         DocumentBuilder documentBuilder = 
createDocumentBuilder(getDocumentBuilderFactory(exchange));
         return documentBuilder.parse(new ByteArrayInputStream(data));
     }
 
-    @Converter(order = 55)
+    @Converter(order = 55, allowNull = true)
     public Document toDOMDocument(StreamCache cache, Exchange exchange)
             throws IOException, SAXException, ParserConfigurationException {
-        InputStream is = 
exchange.getContext().getTypeConverter().convertTo(InputStream.class, exchange, 
cache);
-        return toDOMDocument(is, exchange);
+        byte[] data = 
exchange.getContext().getTypeConverter().convertTo(byte[].class, exchange, 
cache);
+        if (!looksLikeXml(data)) {
+            return null;
+        }
+        DocumentBuilder documentBuilder = 
createDocumentBuilder(getDocumentBuilderFactory(exchange));
+        return documentBuilder.parse(new ByteArrayInputStream(data));
     }
 
     /**
@@ -1238,4 +1245,48 @@ public class XmlConverter {
             LOG.error(exception.getMessage(), exception);
         }
     }
+
+    /**
+     * Returns {@code true} if the given byte array looks like it could be 
well-formed XML, by inspecting only the first
+     * few bytes.
+     * <p>
+     * Handles UTF-8 BOM, UTF-16 BE/LE BOMs, and leading ASCII whitespace. 
Returns {@code true} for anything whose first
+     * non-BOM, non-whitespace byte is {@code <}, which means it will not 
reject valid XML. Its only purpose is to
+     * short-circuit the expensive {@code DocumentBuilder.parse()} call for 
obviously non-XML content (empty body,
+     * JSON/HTML error pages, plain text) before any DOM allocation occurs.
+     *
+     * @param  data the bytes to inspect (may be null or empty)
+     * @return      {@code true} if the content may be XML; {@code false} if 
it is definitely not XML
+     */
+    static boolean looksLikeXml(byte[] data) {
+        if (data == null || data.length == 0) {
+            return false;
+        }
+        int offset = 0;
+        // skip UTF-8 BOM (EF BB BF)
+        if (data.length >= 3
+                && (data[0] & 0xFF) == 0xEF
+                && (data[1] & 0xFF) == 0xBB
+                && (data[2] & 0xFF) == 0xBF) {
+            offset = 3;
+        } else if (data.length >= 2) {
+            // UTF-16 BE (FE FF) or LE (FF FE) BOM — XML parsers handle these 
natively
+            int b0 = data[0] & 0xFF;
+            int b1 = data[1] & 0xFF;
+            if ((b0 == 0xFE && b1 == 0xFF) || (b0 == 0xFF && b1 == 0xFE)) {
+                return true;
+            }
+        }
+        // skip leading ASCII whitespace
+        while (offset < data.length) {
+            byte b = data[offset];
+            if (b == ' ' || b == '\t' || b == '\r' || b == '\n') {
+                offset++;
+            } else {
+                break;
+            }
+        }
+        return offset < data.length && data[offset] == '<';
+    }
+
 }
diff --git 
a/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java
 
b/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java
new file mode 100644
index 000000000000..7a7d1937c464
--- /dev/null
+++ 
b/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.converter.jaxp;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Verifies the prolog guard in {@link XmlConverter}.
+ *
+ * <p>
+ * {@link XmlConverter#looksLikeXml(byte[])} unit tests confirm the helper 
correctly identifies XML vs non-XML content.
+ * Integration tests via {@link XmlConverter#toDOMDocument(byte[], 
org.apache.camel.Exchange)} confirm that non-XML
+ * payloads return {@code null} (allowing the type-converter framework to fall 
through gracefully) rather than
+ * triggering expensive DOM construction and a {@code SAXParseException: 
Content is not allowed in prolog}.
+ */
+class XmlConverterPrologTest {
+
+    // ---- looksLikeXml unit tests ----
+
+    @Test
+    void testLooksLikeXmlNullReturnsFalse() {
+        assertThat(XmlConverter.looksLikeXml(null)).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlEmptyReturnsFalse() {
+        assertThat(XmlConverter.looksLikeXml(new byte[0])).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlJsonBodyReturnsFalse() {
+        assertThat(XmlConverter.looksLikeXml("{\"error\":\"bad 
request\"}".getBytes(StandardCharsets.UTF_8))).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlPlainTextReturnsFalse() {
+        assertThat(XmlConverter.looksLikeXml("some plain 
text".getBytes(StandardCharsets.UTF_8))).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlHttpStatusLineReturnsFalse() {
+        assertThat(XmlConverter.looksLikeXml(
+                "HTTP/1.1 500 Internal Server 
Error".getBytes(StandardCharsets.UTF_8))).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlUtf8BomOnlyReturnsFalse() {
+        byte[] bomOnly = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+        assertThat(XmlConverter.looksLikeXml(bomOnly)).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlUtf8BomFollowedByJsonReturnsFalse() {
+        byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+        byte[] body = "{\"k\":\"v\"}".getBytes(StandardCharsets.UTF_8);
+        byte[] data = new byte[bom.length + body.length];
+        System.arraycopy(bom, 0, data, 0, bom.length);
+        System.arraycopy(body, 0, data, bom.length, body.length);
+        assertThat(XmlConverter.looksLikeXml(data)).isFalse();
+    }
+
+    @Test
+    void testLooksLikeXmlValidXmlDeclarationReturnsTrue() {
+        assertThat(XmlConverter.looksLikeXml(
+                "<?xml 
version=\"1.0\"?><root/>".getBytes(StandardCharsets.UTF_8))).isTrue();
+    }
+
+    @Test
+    void testLooksLikeXmlValidXmlNoDeclarationReturnsTrue() {
+        
assertThat(XmlConverter.looksLikeXml("<root><child/></root>".getBytes(StandardCharsets.UTF_8))).isTrue();
+    }
+
+    @Test
+    void testLooksLikeXmlLeadingWhitespaceBeforeTagReturnsTrue() {
+        assertThat(XmlConverter.looksLikeXml("  
\t\r\n<root/>".getBytes(StandardCharsets.UTF_8))).isTrue();
+    }
+
+    @Test
+    void testLooksLikeXmlUtf8BomFollowedByXmlReturnsTrue() {
+        byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+        byte[] body = "<root/>".getBytes(StandardCharsets.UTF_8);
+        byte[] data = new byte[bom.length + body.length];
+        System.arraycopy(bom, 0, data, 0, bom.length);
+        System.arraycopy(body, 0, data, bom.length, body.length);
+        assertThat(XmlConverter.looksLikeXml(data)).isTrue();
+    }
+
+    @Test
+    void testLooksLikeXmlUtf16BeBomReturnsTrue() {
+        byte[] data = { (byte) 0xFE, (byte) 0xFF, 0x00, '<' };
+        assertThat(XmlConverter.looksLikeXml(data)).isTrue();
+    }
+
+    @Test
+    void testLooksLikeXmlUtf16LeBomReturnsTrue() {
+        byte[] data = { (byte) 0xFF, (byte) 0xFE, '<', 0x00 };
+        assertThat(XmlConverter.looksLikeXml(data)).isTrue();
+    }
+
+    // ---- toDOMDocument prolog-guard integration tests ----
+
+    @Test
+    void testToDOMDocumentEmptyByteArrayReturnsNull() throws Exception {
+        XmlConverter converter = new XmlConverter();
+        assertThat(converter.toDOMDocument(new byte[0], null)).isNull();
+    }
+
+    @Test
+    void testToDOMDocumentJsonBodyReturnsNull() throws Exception {
+        XmlConverter converter = new XmlConverter();
+        byte[] json = 
"{\"status\":\"error\"}".getBytes(StandardCharsets.UTF_8);
+        assertThat(converter.toDOMDocument(json, null)).isNull();
+    }
+
+    @Test
+    void testToDOMDocumentPlainTextReturnsNull() throws Exception {
+        XmlConverter converter = new XmlConverter();
+        byte[] text = "HTTP/1.1 503 Service 
Unavailable".getBytes(StandardCharsets.UTF_8);
+        assertThat(converter.toDOMDocument(text, null)).isNull();
+    }
+
+    @Test
+    void testToDOMDocumentInputStreamJsonBodyThrows() {
+        XmlConverter converter = new XmlConverter();
+        byte[] json = 
"{\"status\":\"error\"}".getBytes(StandardCharsets.UTF_8);
+        assertThatThrownBy(() -> converter.toDOMDocument(new 
ByteArrayInputStream(json), null))
+                .isInstanceOf(Exception.class);
+    }
+}

Reply via email to