Use RestTemplate with object as data and application/x-www-form-urlencoded content type?

Viewed 26995

I need to post an object (e.g. not a MultiValueMap) via a RestTemplate with the content type application/x-www-form-urlencoded. When I try to do so ...

HttpHeaders headers = new HttpHeaders();
HttpEntity request;

headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED)

// data is some generic type
request = new HttpEntity<>(data, headers);

// clazz is the Class<T> being returned
restTemplate.exchange(url, method, request, clazz)

... I get the following error:

org.springframework.web.client.RestClientException: Could not write request: no suitable HttpMessageConverter found for request type [com.whatever.MyRequestPayload] and content type [application/x-www-form-urlencoded]

Here is what I see within restTemplate.getMessageConverters():

message converters

Why don't I want to provide a MultiValueMap? Two reasons:

  1. this is general purpose code which is used to send requests to multiple endpoints, so adding an overload specifically for x-www-form-urlencoded will only complicate things
  2. it doesn't seem like I should have to -- I just don't know which HttpMessageConverter needs to be used to support converting objects to a x-www-form-urlencoded string
6 Answers

I ended up having to write a custom HTTP message converter which takes any object and writes it out as www-form-urlencoded content to the request body:

Usage

RestTemplate template = new RestTemplate(...);

template.getMessageConverters().add(new ObjectToUrlEncodedConverter(mapper));

ObjectToUrlEncodedConverter

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.List;

public class ObjectToUrlEncodedConverter implements HttpMessageConverter
{
    private static final String Encoding = "UTF-8";

    private final ObjectMapper mapper;

    public ObjectToUrlEncodedConverter(ObjectMapper mapper)
    {
        this.mapper = mapper;
    }

    @Override
    public boolean canRead(Class clazz, MediaType mediaType)
    {
        return false;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType)
    {
        return getSupportedMediaTypes().contains(mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes()
    {
        return Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED);
    }

    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException
    {
        throw new NotImplementedException();
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException
    {
        if (o != null)
        {
            String body = mapper
                .convertValue(o, UrlEncodedWriter.class)
                .toString();

            try
            {
                outputMessage.getBody().write(body.getBytes(Encoding));
            }
            catch (IOException e)
            {
                // if UTF-8 is not supporter then I give up
            }
        }
    }

    private static class UrlEncodedWriter
    {
        private final StringBuilder out = new StringBuilder();

        @JsonAnySetter
        public void write(String name, Object property) throws UnsupportedEncodingException
        {
            if (out.length() > 0)
            {
                out.append("&");
            }

            out
                .append(URLEncoder.encode(name, Encoding))
                .append("=");

            if (property != null)
            {
                out.append(URLEncoder.encode(property.toString(), Encoding));
            }
        }

        @Override
        public String toString()
        {
            return out.toString();
        }
    }
}

Reason: there is no converter can convert your java object into request body in x-www-form-urlencoded format.

Solution1: create this kind of converter, as what @Josh M. posts.

Solution2: convert your java object into MultiValueMap, and there is already a converter named FormHttpMessageConverter in spring boot which will convert MultiValueMap into request body in x-www-form-urlencoded format automatically.

So in solution2, all you need is to convert your java object into MultiValueMap:

        MultiValueMap<String, String> bodyPair = new LinkedMultiValueMap();
        bodyPair.add(K1, V1);
        bodyPair.add(K1, V2);
        bodyPair.add(K2, V2);
        ...

K1, V1, K2, V2, ..., means the field names and corresponding values in your java object. All the fields you declared in your java class need to be added. If there are too many fields, consider using Java Reflection.

Have you tried something like adding MappingJackson2HttpMessageConverter to the RestTemplate

restTemplate.getMessageConverters().add(getMappingJackson2HttpMessageConverter());

public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
    mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED));
    return mappingJackson2HttpMessageConverter;
}

If you are using WebMvcConfigurerAdapter then you might get such errors. Also WebMvcConfigurerAdapter is deprecated from Spring 5.x.x. Try using the WebMvcConfigurer. Once done you will have to override:

@Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {}

You can use FormHttpMessageConverter for content-type "application/x-www-form-urlencoded" as below and send data in RestTemplate post method for body as MultiValueMap<String, String>

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate" lazy-init="true">
    <constructor-arg name="requestFactory" ref="bufferingClientHttpRequestFactory" />
    <property name="errorHandler" ref="responeErrorHandler" />
    <property name="messageConverters">
    <list>
        <bean class="org.springframework.http.converter.FormHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </list>
</property>
</bean>

Java Config Version

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();          
    restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    return restTemplate 
}
Related