javaspringspring-bootspring-mvc

RestClient message converters unable to parse ISO-8859-1 response from remote server


I'm using Spring boot 3.4.4 calling a remote xml service where the response is in ISO-8859-1 encoding.

Response from server ->

HTTP/1.1 200 OK
Content-type: application/xml;charset=iso-8859-1
Date: Fri, 11 Apr 2025 06:51:09 +0200
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'; sandbox; default-src 'none'
Content-length: 1163

<?xml version="1.0"?>
<programs>
    <ES_PRODUCT>
        <p_product_type>Episode</p_product_type>
        <p_product_productcode>123</p_product_productcode>
        <p_product_originaltitle>Bue, pil, økser og stædighed</p_product_originaltitle>
        <prd_external_reference>4714857215527</prd_external_reference>
        <image1Sequence>0</image1Sequence>
        <image1Type>Standard</image1Type>
        <image1URN>https://foo.bar</image1URN>
    </ES_PRODUCT>
</programs>

The restclient is configured to accept ISO-8859-1 encoding ->

@Bean(name = "woRestClient")
RestClient woRestClient(RestClient.Builder builder) {
    var factory = new JdkClientHttpRequestFactory(createHttpClient());
    return builder
        .baseUrl(configuration.endpointUrl)
        .messageConverters(httpMessageConverters -> {
            httpMessageConverters.forEach(httpMessageConverter -> {
                if (httpMessageConverter instanceof Jaxb2RootElementHttpMessageConverter converter) {
                    converter.setDefaultCharset(StandardCharsets.ISO_8859_1);
                }
            });
        })
        .requestFactory(factory)
        .build();
}

private HttpClient createHttpClient() {
    return HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_1_1)
        .connectTimeout(Duration.ofSeconds(5))
        .build();
}

The service ->

@Service
public class TheService {
    private final RestClient restClient;

    public TheService(RestClient restClient) {
        this.restClient = restClient;
    }

    public Programs getStuff() {
        var programs = restClient.get()
            .uri("/someuri")
            .retrieve()
            .body(Programs.class);
        return programs;
    }

}

And the pom ->

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.woodstox</groupId>
            <artifactId>woodstox-core</artifactId>
            <version>6.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-xjc-plugin</artifactId>
                <version>4.0.0</version>
                <executions>
                    <execution>
                        <id>xjc</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>xsdtojava</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <xsdOptions>
                        <xsdOption>
                            <xsd>${basedir}/src/main/spec/myschema.xsd</xsd>
                            <bindingFile>${basedir}/src/main/spec/binding.xjb</bindingFile>
                            <extension>true</extension>
                        </xsdOption>
                    </xsdOptions>
                </configuration>
            </plugin>

        </plugins>
    </build>

</project>

When calling the service the response still seems to be treated as a utf-8 stream since I get an error of Invalid byte 1 of 1-byte UTF-8 sequence.] ->

org.springframework.web.client.RestClientException: Error while extracting response for type [generated.Programs] and content type [application/xml;charset=iso-8859-1]

    at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:261)
    at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.readBody(DefaultRestClient.java:814)
    at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.lambda$body$0(DefaultRestClient.java:745)
    at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:574)
    at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchange(DefaultRestClient.java:535)
    at org.springframework.web.client.RestClient$RequestHeadersSpec.exchange(RestClient.java:677)
    at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.executeAndExtract(DefaultRestClient.java:809)
    at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.body(DefaultRestClient.java:745)
    at com.example.demo.whatson.WhatsonService.getProduction(WhatsonService.java:24)
    at com.example.demo.whatson.WhatsonServiceTest.testGetProduction(WhatsonServiceTest.java:18)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: Could not unmarshal to [class generated.Programs]: jakarta.xml.bind.UnmarshalException
 - with linked exception:
[com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 1 of 1-byte UTF-8 sequence.]
    at org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter.readInternal(AbstractXmlHttpMessageConverter.java:78)
    at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:198)
    at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:244)
    ... 12 more
Caused by: jakarta.xml.bind.UnmarshalException
 - with linked exception:
[com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 1 of 1-byte UTF-8 sequence.]
    at jakarta.xml.bind.helpers.AbstractUnmarshallerImpl.createUnmarshalException(AbstractUnmarshallerImpl.java:294)
    at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.UnmarshallerImpl.createUnmarshalException(UnmarshallerImpl.java:539)
    at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:224)
    at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal(UnmarshallerImpl.java:189)
    at jakarta.xml.bind.helpers.AbstractUnmarshallerImpl.unmarshal(AbstractUnmarshallerImpl.java:134)
    at jakarta.xml.bind.helpers.AbstractUnmarshallerImpl.unmarshal(AbstractUnmarshallerImpl.java:117)
    at org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter.readFromSource(Jaxb2RootElementHttpMessageConverter.java:141)
    at org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter.readInternal(AbstractXmlHttpMessageConverter.java:72)
    ... 14 more
Caused by: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 1 of 1-byte UTF-8 sequence.
    at java.xml/com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.invalidByte(UTF8Reader.java:702)
    at java.xml/com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(UTF8Reader.java:568)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1699)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.skipChar(XMLEntityScanner.java:1375)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2762)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:605)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:114)
    at java.xml/com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:542)
    at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:889)
    at java.xml/com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:825)
    at java.xml/com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
    at java.xml/com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1224)
    at java.xml/com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:637)
    at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:218)
    ... 19 more

Any help to set up spring-boot to accept iso encoding would be appreciated.


Solution

  • I also made a try : https://framagit.org/FBibonne/poc-java/-/tree/iso8859?ref_type=heads : this litle project tries to reproduce your context with the class ConfigRestClient which provides RestClients to retrieve Product entites (simplified version of your )

    Then I made tests mocking a server which serves xml encoding with ISO-8859-1 : https://framagit.org/FBibonne/poc-java/-/blob/iso8859/src/test/java/poc/java/springrestclient/ConfigRestClientTest.java?ref_type=heads : each test configures a mock server to return xml, gets a ResClient from ConfigRestClient and calls retrieve with the RestClient as your method getStuff() does.

    The different tests try to explain the problem and suggest a fix :

    If you introduced such a RestClient in your application, you would only use it for the problematic endpoint thanks to @Qualifier adn/or @Primary.

    I don't think the problem resides in Spring RestClient : in fact, it seems impossible to set up externally the encoding used by Jaxb unmarshalling : see Override declared encoding during unmarshalling with JAXB : the suggested solution is to process byte decoding outside jaxb to control encoding. I neither found in Jaxb a way to do it : jakarta.xml.bind.helpers.AbstractUnmarshallerImpl#setProperty does not support any property set (raise exceptions)