Solución al error de @RequestBody en Spring Boot con tipo de contenido application/x-www-form-urlencoded

Primera solución: Adaptador de controladores para manejar form-urlencoded

En Spring Boot, al utilizar la anotación @RequestBody con el tipo de contenido 'application/x-www-form-urlencoded;charset=UTF-8', se produce el error Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported. Por ejemplo, el siguiente código fallará:

    @RequestMapping(value = "/act/service/model/{modelId}/save", method = RequestMethod.POST)
    public void saveModel(@PathVariable String modelId, @RequestBody MultiValueMap<String, String> formData) {
        // Lógica de negocio
    }

Este código funciona en Spring MVC tradicional debido a la configuración de <mvc:annotation-driven>, que establece convertidores de mensajes específicos. En Spring MVC 3.1, esta configuración incluía:

<!-- Mapeo de solicitudes por anotación -->
    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">        
        <property name="interceptors">
            <list>  
                <ref bean="logInterceptor"/>   <!-- Interceptores personalizados -->
            </list>        
        </property>        
    </bean>      
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">  
            <list>  
                <ref bean="byteConverter" />  
                <ref bean="stringConverter" />  
                <ref bean="resourceConverter" />  
                <ref bean="sourceConverter" />  
                <ref bean="formConverter" />  
                <ref bean="jaxbConverter" />  
                <ref bean="jsonConverter" />  
            </list>  
        </property>  
    </bean>  
    <!-- Definición de convertidores de mensajes -->

En versiones más recientes de Spring Boot, el RequestMappingHandlerAdapter no incluye por defecto el convertidor JSON. Para solucionar esto, podemos configurarlo manualmente:

@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
        
        List<HttpMessageConverter<?>> converters = new ArrayList<>(adapter.getMessageConverters());

        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        MediaType plainText = new MediaType(MediaType.TEXT_PLAIN, Charset.forName("UTF-8"));
        supportedMediaTypes.add(plainText);
        MediaType jsonType = new MediaType(MediaType.APPLICATION_JSON, Charset.forName("UTF-8"));
        supportedMediaTypes.add(jsonType);
        jsonConverter.setSupportedMediaTypes(supportedMediaTypes);
        
        converters.add(jsonConverter);
        
        adapter.setMessageConverters(converters);
        return adapter;
    }
}

Con esta configuración, el error desaparece y los parámetros se reciben correctamente.

Segunda solución: Resolver de argumentos personalizado para múltiples tipos de contenido

Crear un resolvedor de argumentos personalizado en Spring Boot para permitir que @RequestBody acepte además de JSON, el tipo de contenido application/x-www-form-urlencoded

Conceptos básicos: En SpringBoot, cuando un método del Controlador tiene un parámetro anotado con @RequestBody, el parámetro es procesado por RequestResponseBodyMethodProcessor. Si el Content-Type es application/x-www-form-urlencoded, se produce un error Unsupported Media Type, lo que exige que el Content-Type debe ser application/json.

Normalmente, si un parámetro del Controlador no tiene la anotación @RequestBody, es procesado por ServletModelAttributeMethodProcessor, que sí soporta application/x-www-form-urlencoded.

Requisito: Implementar que un parámetro con @RequestBody pueda recibir tanto solicitudes con Content-Type application/json como application/x-www-form-urlencoded.

Enfoque de implementación: Crear un resolvedor de argumentos personalizado que, cuando detecte @RequestBody, verifique el Content-Type. Si es application/x-www-form-urlencoded, utilice ServletModelAttributeMethodProcessor; de lo contrario, utilice RequestResponseBodyMethodProcessor.

Implementación:

Resolvedor de argumentos personalizado:

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
 
import javax.servlet.http.HttpServletRequest;
 
/**
 * Resolvedor de argumentos personalizado para extender RequestResponseBodyMethodProcessor
 *
 * Este resolvedor permite que parámetros con @RequestBody acepten:
 * 1. Content-Type application/x-www-form-urlencoded (usando ServletModelAttributeMethodProcessor)
 * 2. Content-Type application/json (usando RequestResponseBodyMethodProcessor)
 */
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
 
    private final RequestResponseBodyMethodProcessor jsonProcessor;
    private final ServletModelAttributeMethodProcessor formProcessor;
 
    public CustomArgumentResolver(RequestResponseBodyMethodProcessor jsonProcessor,
                                ServletModelAttributeMethodProcessor formProcessor) {
        this.jsonProcessor = jsonProcessor;
        this.formProcessor = formProcessor;
    }
 
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }
 
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        final String formUrlEncoded = "application/x-www-form-urlencoded";
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
 
        if (request == null) {
            throw new IllegalStateException("La solicitud HTTP no debe ser nula");
        }
        
        String contentType = request.getContentType();
        
        // Si es form-urlencoded, usar el procesador de formularios
        if (formUrlEncoded.equals(contentType)) {
            return formProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        }
        
        // De lo contrario, usar el procesador JSON por defecto
        return jsonProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
}

Regsitro del resolvedor de argumentos:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
 
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
 
/**
 * Configuración para registrar el resolvedor de argumentos personalizado
 */
@Configuration
public class ArgumentResolverConfig {
 
    private final RequestMappingHandlerAdapter handlerAdapter;
 
    public ArgumentResolverConfig(RequestMappingHandlerAdapter handlerAdapter) {
        this.handlerAdapter = handlerAdapter;
    }
 
    @PostConstruct
    public void registerCustomResolver() {
        List<HandlerMethodArgumentResolver> existingResolvers = handlerAdapter.getArgumentResolvers();
        CustomArgumentResolver customResolver = createCustomResolver(existingResolvers);
        
        // Crear nueva lista de resolvedores con nuestro resolvedor personalizado
        List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>(existingResolvers.size() + 1);
        newResolvers.add(customResolver);
        newResolvers.addAll(existingResolvers);
        
        handlerAdapter.setArgumentResolvers(newResolvers);
    }
 
    private CustomArgumentResolver createCustomResolver(List<HandlerMethodArgumentResolver> resolvers) {
        RequestResponseBodyMethodProcessor jsonProcessor = null;
        ServletModelAttributeMethodProcessor formProcessor = null;
 
        if (resolvers == null) {
            throw new IllegalStateException("La lista de resolvedores no debe ser nula");
        }
        
        for (HandlerMethodArgumentResolver resolver : resolvers) {
            if (jsonProcessor != null && formProcessor != null) {
                break;
            }
            
            if (resolver instanceof RequestResponseBodyMethodProcessor) {
                jsonProcessor = (RequestResponseBodyMethodProcessor) resolver;
                continue;
            }
            
            if (resolver instanceof ServletModelAttributeMethodProcessor) {
                formProcessor = (ServletModelAttributeMethodProcessor) resolver;
            }
        }
        
        if (jsonProcessor == null || formProcessor == null) {
            throw new IllegalStateException("Ambos procesadores (JSON y Form) deben estar disponibles");
        }
        
        return new CustomArgumentResolver(jsonProcessor, formProcessor);
    }
}

Etiquetas: Spring Boot RequestBody HTTP Message Converter Content-Type argument resolver

Publicado el 6-7 05:09