javaspring-bootspring-mvccontent-negotiation

How do you configure spring boot 2 to return xml by default?


To start with I've read this:

Spring boot - setting default Content-type header if it's not present in request

The older version of this worked on spring boot 1. However when receiving request with the following accept header Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"

The response is in json.

I've put in a class

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(
            ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_XML);
    }

}

And I can see that the defaultContentType stratedgy is being set. However it its being overwritten by the the AcceptHeaderConfig stratedgy.

It looks like the defaultContentType is only used as a fallback.

Note that the same code in spring boot 1 worked and defaulted to XML.


Complete Example

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.annotation.XmlRootElement;

@SpringBootApplication
@RestController
public class CnApp {

    @RequestMapping("/")
    public Person person(HttpServletRequest request, ModelMap model){
        return new Person();
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(CnApp.class, args);
    }

    @XmlRootElement
    public static class Person {
        public String firstName = "Jon";
        public String lastName = "Doe";
    }

    @Configuration
    public static class ServerConfig implements WebMvcConfigurer {
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            configurer.defaultContentType(MediaType.APPLICATION_XML);
        }

    }

}

as you can see by running curl localhost:8080 -H"Accept: text/html, image/gif, image/jpg;q=0.2, */*;q=0.2" It defaults to json even though XML is default


From comment I posted below

The issue is with the old version of spring we can send with an accept header and get requests for that it defaults to XML. However JSON is still supported.

So when an accept header comes in that supports both JSON and XML at same specificity we need to return XML.


Solution

  • Investigating this further. What @thepaoloboi suggested in his answer is correct the defaultMessageConverter does happen only if no other form of content negotiation happened.

    To remedy this I've stepped through the code that the the org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor uses and I see its dependant on the order of the coverters that have been configured.

    So as a hack the following works both in spring 1 and spring 2.

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.converter.xml.Jaxb2CollectionHttpMessageConverter;
    import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.xml.bind.annotation.XmlRootElement;
    import java.util.List;
    
    @SpringBootApplication
    @RestController
    public class CnApp {
    
        @RequestMapping("/")
        public Person person(HttpServletRequest request, ModelMap model){
            return new Person();
        }
    
        public static void main(String[] args) throws Exception {
            SpringApplication.run(CnApp.class, args);
        }
    
        @XmlRootElement
        public static class Person {
            public String firstName = "Jon";
            public String lastName = "Doe";
        }
    
        @Configuration
        public static class ServerConfig extends WebMvcConfigurerAdapter {
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                configurer.defaultContentType(MediaType.APPLICATION_XML);
            }
    
            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    
                converters.add(0, new Jaxb2CollectionHttpMessageConverter<>());
                converters.add(0, new Jaxb2RootElementHttpMessageConverter());
                System.out.println("Converters:" + converters);
            }
        }
    
    }
    

    How this works is its setting the Jaxb2 converters with a higher priority than the jackson convertors.

    This can be tested as follows

    curl localhost:8080 -H"Accept: text/html, image/gif, image/jpg;q=0.2, */*;q=0.2"
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><firstName>Jon</firstName><lastName>Doe</lastName></person>%
    
    curl localhost:8080 -H"Accept: text/html, image/gif, image/jpg;q=0.2, application/json, */*;q=0.2"
    {"firstName":"Jon","lastName":"Doe"}%            
    

    Note that if application/json is specificed anywhere in the header this is still preferred.

    This does still feel like a hack and it would be good if there was a way to sort preferred mime types without resorting to adding or reordering converters