Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -696,15 +697,31 @@ public Element toDOMElement(Node node) throws TransformerException {
@Converter(order = 54)
public Document toDOMDocument(byte[] data, Exchange exchange)
throws IOException, SAXException, ParserConfigurationException {
if (!looksLikeXml(data)) {
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)
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)) {
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);
}

/**
Expand Down Expand Up @@ -1226,4 +1243,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.
* <p>
* The check handles:
* <ul>
* <li>UTF-8 BOM (EF BB BF) — skipped before the prolog test</li>
* <li>UTF-16 BE/LE BOM (FE FF / FF FE) — accepted, XML parsers handle these natively</li>
* <li>Leading ASCII whitespace — skipped before the {@code <} test</li>
* <li>Any content whose first non-BOM, non-whitespace byte is not {@code <} — rejected</li>
* </ul>
* <p>
* This is intentionally a fast, conservative check: it returns {@code true} for anything that <em>could</em> 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] == '<';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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.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 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[], 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.
*
* <p>
* 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));
}

@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(
"<?xml version=\"1.0\"?><root/>".getBytes(StandardCharsets.UTF_8)));
}

@Test
void looksLikeXml_validXmlNoDeclarationReturnsTrue() {
assertTrue(XmlConverter.looksLikeXml("<root><child/></root>".getBytes(StandardCharsets.UTF_8)));
}

@Test
void looksLikeXml_leadingWhitespaceBeforeTagReturnsTrue() {
assertTrue(XmlConverter.looksLikeXml(" \t\r\n<root/>".getBytes(StandardCharsets.UTF_8)));
}

@Test
void looksLikeXml_utf8BomFollowedByXmlReturnsTrue() {
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);
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));
}

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