相关文章推荐
Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

How can I register a custom HttpMessageConverter to deal with an invalid Content-Type in Spring?

Ask Question

I'm writing code to POST data to a third party API using a RestTemplate . That API responds with the content-type text;charset=UTF-8 , and Spring throws an InvalidMediaTypeException because that content type does not contain a / . Is it possible to indicate to Spring that a content-type of text should be treated the same as a content type of text/plain ? If so, how do I accomplish this?

This is the code that's causing the problem. I can't show the URL , but I assume that doesn't really matter.

// Make the body of the request.
MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();
body.add("Customer Street", "a test street!");
// Make the headers of the request.
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Arrays.asList(MediaType.TEXT_PLAIN));
// Create an HTTP entity.
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<MultiValueMap<String, String>>(body, headers);
// Get a rest template
RestTemplate rest = new RestTemplate();
// Post the data.
String resp = null;
try {
    resp = rest.postForObject(URL, entity, String.class);
} catch (InvalidMediaTypeException e) {
    e.printStackTrace();
    return;

Related SO Questions

This question describes almost exactly the problem I'm having. The accepted answer to that question is, essentially, "See this other question," which I did; it's described below.

In this question (linked in the answer to the above), istibekesi asks about using a custom content-type of myXml, and provides an example configuration that did not work. Brian Clozel's answer helped me understand some things about content-types that I didn't understand before, but I'm still confused on these points:

  • Brian says that the given configuration should register myXml as a path extension / parameter for negotiating to application/xml. My initial understanding was that Brian meant, "Requests with the accept header set to myXml should be treated the same as requests with the accept header set to application/xml." However, now I'm pretty sure Brian meant, "Requests ending in .myXml or with the query parameter format=myXml should be treated as application/xml." Is my second interpretation correct? If so, why doesn't the given configuration force Spring to treat requests with the accept header set to myXml as application/xml?

  • Brian states that what istibekesi should probably do is register an HttpMessageConverter and then register it with application/xml and myXml. I think I understand what Brian means by saying, "register an HttpMessageConverter," however I can't figure out how to register a custom media type (such as myXml) to use that HttpMessageConverter.

  • Unfortunately, Brian's advice of "use a media type like application/vnd.foobar.v.1.0+xml" isn't helpful to me, because I have no control over the content-type of the response that I'm sent. I have tried setting the accept headers of my request to text/plain, but it didn't change the response.

  • Other Research

    The stack trace of the exception that Spring is throwing is

    org.springframework.http.InvalidMediaTypeException: Invalid mime type "text;charset=UTF-8": does not contain '/'
        at org.springframework.http.MediaType.parseMediaType(MediaType.java:452)
        at org.springframework.http.HttpHeaders.getContentType(HttpHeaders.java:745)
        at org.springframework.web.client.HttpMessageConverterExtractor.getContentType(HttpMessageConverterExtractor.java:114)
        at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:85)
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:655)
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
        at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:380)
        at com.agileBTS.hellosignTest.App.main(App.java:47)
    Caused by: org.springframework.util.InvalidMimeTypeException: Invalid mime type "text;charset=UTF-8": does not contain '/'
        at org.springframework.util.MimeTypeUtils.parseMimeType(MimeTypeUtils.java:256)
        at org.springframework.http.MediaType.parseMediaType(MediaType.java:449)
    

    I went through the source code of every function on the stack trace to see if I could figure out what was going on, and I can see clearly that the parseMimeType method (the first method to throw an exception) is pretty straightforward: if the mime-type does not contain a /, it throws an exception. I don't understand how any code is going to get around this, unless I subclass MimeTypeUtils and force Spring to use my subclass. Is that what's required? That seems very difficult.

    Updates

    In their answer, Sean Carroll suggested that I register the "text" mime type using the line

    c.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.parseMediaType("text")));
    

    However, if you check out the source code for the #parseMediaType method here at line 487, you'll see that #parseMediaType hands off most of the work to the MimeTypeUtils#parseMimeType method. Looking at that source code here starting at line 176, it's clear the #parseMimeType will throw an IllegalMimeTypeException at line 193, because "text" does not contain a / (in fact, this is the exact line of code that throws the IllegalMimeTypeException in my application). What I need is a way around this.

    After testing, I've determined that configuring a Content Negotiation Manager also does not work in my case. Based on this tutorial, I think it's clear that the XML configuration:

    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="mediaTypes">
                <entry key="json" value="application/json" />
                <entry key="xml" value="application/xml" />
        </property>
    </bean>
    

    is equivalent to this Java configuration:

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("json", MediaType.APPLICATION_JSON);
    

    Taking a look at the ContentNegotiationConfigurer#mediaType method documentation here, I saw the line, "Add a mapping from a key, extracted from a path extension or a query parameter..." (emphasis mine); I guess excluding "accept headers" from that quote was intentional.

    This is how I interprets Brian's answer. I'm assuming you are using java configuration. To register an HttpMessageConverter you would do something like the following

        @Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            super.configureMessageConverters(converters);
            StringHttpMessageConverter stringMessageConverter = new StringHttpMessageConverter();
            // add support for "text" media type. This may require Charset of UTF-8
            c.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.parseMediaType("text")));
            converters.add(stringMessageConverter);
    Edit: How about using content negotiation manager to map text to text/plain? From javadocs

    For the path extension and parameter strategies you may explicitly add MediaType mappings. This will be used to resolve path extensions or a parameter value such as "json" to a media type such as "application/json".

    The docs explicitly mention extensions and parameters and I'm not completely sure but it may also work on accept headers (I would need to dig into the code more). If it doesn't you may need to look at a custom ContentNegotiationStrategy

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="mediaTypes" >
            <value>
                text=plain/text
            </value>
        </property>
    </bean>
                    Hi Sean, thanks for the response. Unfortunately I'm using XML configuration, but I'll look into converting your suggestion to an XML equivalent and get back to you about how it goes.
    – RobsterLobster
                    May 25, 2017 at 21:40
                    Hi again Sean. I've added an update to my question describing why I believe your answer won't work. If you have any other ideas, I'm all ears.
    – RobsterLobster
                    May 25, 2017 at 22:48
                    Edited my answer. Have you looked at contentNegotiationManager and/or custom ContentNegotiationStrategy?
    – Sean Carroll
                    May 26, 2017 at 3:34
                    Another alternative I'm looking at is to customize RestTemplate. It has a couple of hooks that might be helpful such as acceptHeaderRequestCallback and ResponseExtractor
    – Sean Carroll
                    May 26, 2017 at 4:04
                    This all looks really useful, I'll look into it and get back to you. Thanks so much for your help thus far!
    – RobsterLobster
                    May 26, 2017 at 14:44
    

    It's possible to register a ClientHttpRequestInterceptor with a RestTemplate which allows for editing of client responses before Spring does anything to them. To solve my problem, I created a class that implements ClientHttpRequestInterceptor and overrode the intercept method to replace all instances of text; with application/json; in the Content-Type header. I then registered my ClientHttpRequestInterceptor with my RestTemplate bean definition, so that any time I autowire a RestTemplate, it has this interceptor on it.

    Code for my ClientHttpRequestInterceptor:

    public class ContentTypeTextToTextJson implements ClientHttpRequestInterceptor {
        private static final Logger LOG = LoggerFactory.getLogger(ContentTypeTextToTextJson.class);
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
                throws IOException {
            LOG.debug("intercepting execution");
            // Get the response as normal.
            ClientHttpResponse response = execution.execute(request, body);
            LOG.debug("intercepted response: " + response);
            // Get the headers.
            HttpHeaders headers = response.getHeaders();
            LOG.debug("response had headers: " + headers);
            // Grab all the content types.
            List<String> contentTypes = headers.get("Content-Type");
            LOG.debug("response had content-types: " + contentTypes);
            // Loop over the content-types.
            for(int i = 0; i < contentTypes.size(); i++) {
                String contentType = contentTypes.get(i);
                LOG.debug("processing content type: " + contentType);
                // I'm not sure if it's possible for a content-type to be null, but I guess it's
                // better safe then sorry?
                if(null == contentType) {
                    continue;
                // If it starts with "text;", replace "text" with "text/json" and replace the old content type.
                if(contentType.startsWith("text;")) {
                    contentType = contentType.replaceFirst("text", "application/json");
                    LOG.debug("replacing content type " + contentTypes.get(i) + " with content type " + contentType);
                    contentTypes.set(i, contentType);
            // Return the response.
            return response;
    

    RestTemplate bean definition:

    <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
        <property name="interceptors">
                <bean class="restinterceptors.ContentTypeTextToTextJson" />
            </list>
        </property>
    </bean>
            

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.

     
    推荐文章