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);
+ }
+}