From 08f0c2f562503a16f6cec42fe29c0a48f554a13c Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Thu, 20 Aug 2026 10:48:12 +0530 Subject: [PATCH 1/2] camel-xml-jaxp: add prolog guard to XmlConverter.toDOMDocument to avoid SAXParseException for non-XML content When a ByteArrayInputStreamCache (or byte[]) carrying a non-XML response body (empty, JSON, plain-text HTTP error, BOM-only) is passed to toDOMDocument, the JDK XML parser throws: SAXParseException: Content is not allowed in prolog. Seen in practice via: XmlConverter.toDOMDocument(StreamCache, Exchange) <- CxfPayloadConverter.convertTo() <- DefaultCxfBinding.getBodyFromCamel() Fix: add a cheap static looksLikeXml(byte[]) helper that checks only the first few bytes (handles UTF-8/UTF-16 BOMs and leading whitespace) and returns null from toDOMDocument(byte[], Exchange) and toDOMDocument(StreamCache, Exchange) when content cannot be XML. The @Converter(allowNull=true) annotation tells the Camel type-converter framework that null is a valid non-match result, so it falls through to the next converter gracefully instead of logging an error. Add XmlConverterPrologTest with 13 tests covering null, empty, JSON, plain-text, BOM-only, UTF-8/UTF-16 BOM variants, and valid XML cases. Co-authored-by: Claude --- .../camel/converter/jaxp/XmlConverter.java | 68 +++++++++- .../jaxp/XmlConverterPrologTest.java | 119 ++++++++++++++++++ 2 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java 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 fdb98e05e0022..b38631aa294d4 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 @@ -693,18 +693,26 @@ public Element toDOMElement(Node node) throws TransformerException { * @param exchange is the exchange to be used when calling the converter * @return the parsed document */ - @Converter(order = 54) + @Converter(order = 54, allowNull = true) public Document toDOMDocument(byte[] data, Exchange exchange) throws IOException, SAXException, ParserConfigurationException { + if (!looksLikeXml(data)) { + LOG.debug("Skipping DOM parse: byte[] content does not start with a valid XML prolog"); + 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)) { + LOG.debug("Skipping DOM parse: StreamCache content does not start with a valid XML prolog"); + return null; + } + return toDOMDocument(data, exchange); } /** @@ -1226,4 +1234,56 @@ public void fatalError(SAXParseException exception) throws SAXException { 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. + *

+ * The check handles: + *

+ *

+ * This is intentionally a fast, conservative check: it returns {@code true} for anything that could be XML + * (starts with {@code <}), which means it will not reject e.g. HTML. Its only purpose is to avoid handing obviously + * non-XML content (empty, JSON, plain-text error bodies) to {@code DocumentBuilder.parse()}, which would throw a + * {@code SAXParseException: Content is not allowed in prolog}. + * + * @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, accept as-is + 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 0000000000000..5af34dad9a1a8 --- /dev/null +++ b/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java @@ -0,0 +1,119 @@ +/* + * 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.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the prolog-guard {@link XmlConverter#looksLikeXml(byte[])} which prevents + * {@code SAXParseException: Content is not allowed in prolog} from being thrown when non-XML content (empty body, JSON, + * plain-text HTTP error response, BOM-only) is passed to {@code toDOMDocument}. + * + *

+ * Real-world trigger: {@code ByteArrayInputStreamCache} carrying a non-XML HTTP response body routed through + * {@code CxfPayloadConverter} -> {@code XmlConverter.toDOMDocument(StreamCache, Exchange)}. + */ +class XmlConverterPrologTest { + + @Test + void looksLikeXml_nullReturnsFalse() { + assertFalse(XmlConverter.looksLikeXml(null)); + } + + @Test + void looksLikeXml_emptyReturnsFalse() { + assertFalse(XmlConverter.looksLikeXml(new byte[0])); + } + + @Test + void looksLikeXml_jsonBodyReturnsFalse() { + assertFalse(XmlConverter.looksLikeXml("{\"error\":\"bad request\"}".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_plainTextReturnsFalse() { + assertFalse(XmlConverter.looksLikeXml("some plain text".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_httpStatusLineReturnsFalse() { + // typical non-XML upstream response: HTTP status line or JSON error body + assertFalse(XmlConverter.looksLikeXml( + "HTTP/1.1 500 Internal Server Error".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_utf8BomOnlyReturnsFalse() { + byte[] bomOnly = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF }; + assertFalse(XmlConverter.looksLikeXml(bomOnly)); + } + + @Test + void looksLikeXml_utf8BomFollowedByJsonReturnsFalse() { + 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); + assertFalse(XmlConverter.looksLikeXml(data)); + } + + @Test + void looksLikeXml_validXmlDeclarationReturnsTrue() { + assertTrue(XmlConverter.looksLikeXml( + "".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_validXmlNoDeclarationReturnsTrue() { + assertTrue(XmlConverter.looksLikeXml("".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_leadingWhitespaceBeforeTagReturnsTrue() { + assertTrue(XmlConverter.looksLikeXml(" \t\r\n".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void looksLikeXml_utf8BomFollowedByXmlReturnsTrue() { + byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF }; + byte[] body = "".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); + assertTrue(XmlConverter.looksLikeXml(data)); + } + + @Test + void looksLikeXml_utf16BeBomReturnsTrue() { + // UTF-16 BE BOM: FE FF — XML parsers handle this natively + byte[] data = { (byte) 0xFE, (byte) 0xFF, 0x00, '<' }; + assertTrue(XmlConverter.looksLikeXml(data)); + } + + @Test + void looksLikeXml_utf16LeBomReturnsTrue() { + // UTF-16 LE BOM: FF FE — XML parsers handle this natively + byte[] data = { (byte) 0xFF, (byte) 0xFE, '<', 0x00 }; + assertTrue(XmlConverter.looksLikeXml(data)); + } +} From 406b4a0b1d414e24f91c59a888ec02cd84d1cd13 Mon Sep 17 00:00:00 2001 From: mayurmohan Date: Thu, 20 Aug 2026 12:45:33 +0530 Subject: [PATCH 2/2] camel-xml-jaxp: throw TypeConversionException instead of returning null for non-XML prolog Replace the earlier null-return approach (allowNull=true) with an explicit TypeConversionException when the prolog guard detects content that cannot be XML. Returning null lets the exchange continue with a null body, causing a silent NullPointerException or data loss downstream. Throwing TypeConversionException fires the Camel error handler immediately with a clear, diagnosable message. Also adds TypeConversionException import and three integration tests that assert the exception is thrown (not null returned) for empty body, JSON and plain-text HTTP payloads. Local validation: - mvn formatter:format impsort:sort -- no changes needed - Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 [JDK 21 / Maven 3.9] Co-authored-by: Claude --- .../camel/converter/jaxp/XmlConverter.java | 21 ++++++--- .../jaxp/XmlConverterPrologTest.java | 44 ++++++++++++++++--- 2 files changed, 54 insertions(+), 11 deletions(-) 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 b38631aa294d4..86402fec674b5 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 @@ -75,6 +75,7 @@ import org.apache.camel.Converter; import org.apache.camel.Exchange; import org.apache.camel.StreamCache; +import org.apache.camel.TypeConversionException; import org.apache.camel.support.CamelContextHelper; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; @@ -693,24 +694,32 @@ public Element toDOMElement(Node node) throws TransformerException { * @param exchange is the exchange to be used when calling the converter * @return the parsed document */ - @Converter(order = 54, allowNull = true) + @Converter(order = 54) public Document toDOMDocument(byte[] data, Exchange exchange) throws IOException, SAXException, ParserConfigurationException { if (!looksLikeXml(data)) { - LOG.debug("Skipping DOM parse: byte[] content does not start with a valid XML prolog"); - return null; + throw new TypeConversionException( + data, Document.class, + new IllegalArgumentException( + "Payload does not start with a valid XML prolog" + + " (first bytes do not look like XML —" + + " possible causes: empty body, JSON/HTML error response, wrong encoding)")); } DocumentBuilder documentBuilder = createDocumentBuilder(getDocumentBuilderFactory(exchange)); return documentBuilder.parse(new ByteArrayInputStream(data)); } - @Converter(order = 55, allowNull = true) + @Converter(order = 55) public Document toDOMDocument(StreamCache cache, Exchange exchange) throws IOException, SAXException, ParserConfigurationException { byte[] data = exchange.getContext().getTypeConverter().convertTo(byte[].class, exchange, cache); if (!looksLikeXml(data)) { - LOG.debug("Skipping DOM parse: StreamCache content does not start with a valid XML prolog"); - return null; + throw new TypeConversionException( + data, Document.class, + new IllegalArgumentException( + "Payload does not start with a valid XML prolog" + + " (first bytes do not look like XML —" + + " possible causes: empty body, JSON/HTML error response, wrong encoding)")); } return toDOMDocument(data, exchange); } 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 index 5af34dad9a1a8..7470bfe3be7e2 100644 --- 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 @@ -18,22 +18,31 @@ import java.nio.charset.StandardCharsets; +import org.apache.camel.TypeConversionException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Verifies the prolog-guard {@link XmlConverter#looksLikeXml(byte[])} which prevents - * {@code SAXParseException: Content is not allowed in prolog} from being thrown when non-XML content (empty body, JSON, - * plain-text HTTP error response, BOM-only) is passed to {@code toDOMDocument}. + * Verifies the prolog-guard in {@link XmlConverter}. * *

- * Real-world trigger: {@code ByteArrayInputStreamCache} carrying a non-XML HTTP response body routed through - * {@code CxfPayloadConverter} -> {@code XmlConverter.toDOMDocument(StreamCache, Exchange)}. + * {@link XmlConverter#looksLikeXml(byte[])} unit tests confirm the helper correctly identifies XML vs non-XML content. + * Integration tests via {@link XmlConverter#toDOMDocument(byte[], Exchange)} confirm that non-XML payloads throw + * {@link TypeConversionException} (explicit, diagnosable failure) rather than propagating a + * {@code SAXParseException: Content is not allowed in prolog} from deep inside the JDK parser. + * + *

+ * Real-world trigger: {@code ByteArrayInputStreamCache} carrying a non-XML HTTP response body (JSON error page, empty + * body, BOM-only) routed through {@code CxfPayloadConverter} -> + * {@code XmlConverter.toDOMDocument(StreamCache, Exchange)}. */ class XmlConverterPrologTest { + // ---- looksLikeXml unit tests ---- + @Test void looksLikeXml_nullReturnsFalse() { assertFalse(XmlConverter.looksLikeXml(null)); @@ -116,4 +125,29 @@ void looksLikeXml_utf16LeBomReturnsTrue() { byte[] data = { (byte) 0xFF, (byte) 0xFE, '<', 0x00 }; assertTrue(XmlConverter.looksLikeXml(data)); } + + // ---- toDOMDocument prolog-guard integration tests ---- + + @Test + void toDOMDocument_emptyByteArrayThrowsTypeConversionException() { + XmlConverter converter = new XmlConverter(); + assertThrows(TypeConversionException.class, + () -> converter.toDOMDocument(new byte[0], null)); + } + + @Test + void toDOMDocument_jsonBodyThrowsTypeConversionException() { + XmlConverter converter = new XmlConverter(); + byte[] json = "{\"status\":\"error\"}".getBytes(StandardCharsets.UTF_8); + assertThrows(TypeConversionException.class, + () -> converter.toDOMDocument(json, null)); + } + + @Test + void toDOMDocument_plainTextThrowsTypeConversionException() { + XmlConverter converter = new XmlConverter(); + byte[] text = "HTTP/1.1 503 Service Unavailable".getBytes(StandardCharsets.UTF_8); + assertThrows(TypeConversionException.class, + () -> converter.toDOMDocument(text, null)); + } }