diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java index 11890fa419b..da74365382d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java @@ -16,6 +16,7 @@ package org.springframework.http.converter.xml; +import java.nio.charset.Charset; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -24,7 +25,10 @@ import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.Unmarshaller; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.lang.Nullable; /** * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} @@ -116,4 +120,20 @@ public abstract class AbstractJaxb2HttpMessageConverter extends AbstractXmlHt }); } + /** + * Detect the charset from the given {@link HttpHeaders#getContentType()}. + * @param httpHeaders the current HTTP headers + * @return the charset defined in the content type header, or {@code null} if not found + */ + @Nullable + protected Charset detectCharset(HttpHeaders httpHeaders) { + MediaType contentType = httpHeaders.getContentType(); + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } + else { + return null; + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java index 5730286bafd..d494fcc93af 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java @@ -19,6 +19,7 @@ package org.springframework.http.converter.xml; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -148,7 +149,10 @@ public class Jaxb2CollectionHttpMessageConverter try { Unmarshaller unmarshaller = createUnmarshaller(elementClass); - XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody()); + Charset detectedCharset = detectCharset(inputMessage.getHeaders()); + XMLStreamReader streamReader = (detectedCharset != null) ? + this.inputFactory.createXMLStreamReader(inputMessage.getBody(), detectedCharset.name()) : + this.inputFactory.createXMLStreamReader(inputMessage.getBody()); int event = moveToFirstChildOfRootElement(streamReader); while (event != XMLStreamReader.END_DOCUMENT) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java index 8cc89318184..5417b28500c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.http.converter.xml; import java.io.StringReader; +import java.nio.charset.Charset; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; @@ -135,7 +136,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa @Override protected Object readFromSource(Class clazz, HttpHeaders headers, Source source) throws Exception { try { - source = processSource(source); + source = processSource(source, detectCharset(headers)); Unmarshaller unmarshaller = createUnmarshaller(clazz); if (clazz.isAnnotationPresent(XmlRootElement.class)) { return unmarshaller.unmarshal(source); @@ -160,9 +161,12 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa } } - protected Source processSource(Source source) { + protected Source processSource(Source source, @Nullable Charset charset) { if (source instanceof StreamSource streamSource) { InputSource inputSource = new InputSource(streamSource.getInputStream()); + if (charset != null) { + inputSource.setEncoding(charset.name()); + } try { // By default, Spring will prevent the processing of external entities. // This is a mitigation against XXE attacks. diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java index 19c277a9b7d..be5b42854b1 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.web.testfixture.http.MockHttpInputMessage; @@ -204,6 +205,18 @@ class Jaxb2CollectionHttpMessageConverterTests { .withMessageContaining("\"lol9\""); } + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementListHeaderCharset() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.ISO_8859_1)); + inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1")); + List result = (List) converter.read(rootElementListType, null, inputMessage); + + assertThat(result).as("Invalid result").hasSize(1); + assertThat(result.get(0).type.s).as("Invalid result").isEqualTo("Hellø Wørld"); + } + @XmlRootElement public static class RootElement { diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java index 6c839401533..054f84b7fb2 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -180,6 +180,15 @@ class Jaxb2RootElementHttpMessageConverterTests { .withMessageContaining("DOCTYPE"); } + @Test + void readXmlRootElementHeaderCharset() throws Exception { + byte[] body = "".getBytes(StandardCharsets.ISO_8859_1); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1")); + RootElement result = (RootElement) converter.read(RootElement.class, inputMessage); + assertThat(result.type.s).as("Invalid result").isEqualTo("Hellø Wørld"); + } + @Test void writeXmlRootElement() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();