javaspringspring-mvcpostmanmedia-type

Spring REST Controller content/type not supported for all content types


I'm trying to create a Web Service that consumes an HTML which is later used to create a PDF with iText.

I'm using Postman in order to send my requests to server but everytime I get the following error, no matter which content type I select:

{
    "status": "CONFLICT",
    "code": 40902,
    "message": "Content type 'text/plain' not supported",
    "developerMessage": "null",
    "moreInfoUrl": "class org.springframework.web.HttpMediaTypeNotSupportedException",
    "throwable": null
}

The message changes depending on the content type selected:

"message": "Content type 'text/xml' not supported"
"message": "Content type 'text/html' not supported"

This is my endpoint:

@RequestMapping(path = "/reports/pdf", method = RequestMethod.POST, consumes = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<byte[]> generatePdfReport(@RequestBody String html) throws Exception {
    byte[] pdfBytes = reportsService.generatePdf(html);
    ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(pdfBytes, HttpStatus.OK);
    return response;
}

I've changed consumes attribute to:

to match what I'm sending on Postman.

As well as per @dnault comment, removing it completely from the RequestMapping annotation with the same results.

I've looked into similar questions:

However none of the above and some others that aren't really close to my problem that I've checked already provide an answer that solves this issue.

The sample HTML I'm trying to send to server is:

<table style="border: 1px solid black; font-size: 12px; margin-top: 1px; width: 900px;" id="table_direction">
    <tr>
        <td width="33%" colspan="2">
            <strong>Data1</strong>
        </td>
        <td width="33%" colspan="2">
            <strongData2</strong>
        </td>
        <td width="16%" colspan="1">Foo</td>
        <td width="16%" colspan="1">Bar</td>
    </tr>
    <tr>
        <td colspan="">ID</td>
        <td colspan="">123456</td>
        <td colspan="">Property 1</td>
        <td colspan="">Foo</td>
        <td colspan="">Property 2</td>
        <td colspan="">Bar</td>
    </tr>
</table>

Our configuration is made by Java configuration classes which are as follows:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.company.project")
@PropertySource("classpath:application.properties")

@EnableTransactionManagement // proxyTargetClass = true
@EnableJpaRepositories(basePackages = { "com.company.project.dao", "com.company.project.repository" })

public class HelloWorldConfiguration extends WebMvcConfigurerAdapter {

    @Inject
    Environment env;

    @Value("${jdbc.user}")
    private String userDB;

    @Value("${jdbc.password}")
    private String passDB;

    @Value("${jdbc.url}")
    private String urlDB;

    @Value("${jdbc.driverClassName}")
    private String driverClassName;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public CommonsMultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
    }

    @Bean(name = "dataSource")
    public DriverManagerDataSource dataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setDriverClassName(driverClassName);
        driverManagerDataSource.setUrl(urlDB);
        driverManagerDataSource.setUsername(userDB);
        driverManagerDataSource.setPassword(passDB);
        return driverManagerDataSource;
    }

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        sessionFactory.setPackagesToScan(new String[] { "com.company.project.model" });
        sessionFactory.setHibernateProperties(hibernateProperties());
        return sessionFactory;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
            JpaVendorAdapter jpaVendorAdapter) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource);
        entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
        entityManagerFactoryBean.setPackagesToScan("com.company.project.model");
        entityManagerFactoryBean.setJpaProperties(hibernateProperties());
        return entityManagerFactoryBean;
    }

    private Properties hibernateProperties() {
        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
        jpaProperties.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        jpaProperties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        return jpaProperties;
    }

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean(name = "transactionManager2")
    @Autowired
    @Named("transactionManager2")
    public HibernateTransactionManager transactionManager2(SessionFactory s) {
        HibernateTransactionManager txManager = new HibernateTransactionManager();
        txManager.setSessionFactory(s);
        return txManager;
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        return vendorAdapter;
    }

    public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();

        ObjectMapper mapper = new ObjectMapper();
        Hibernate5Module module = new Hibernate5Module();
        module.disable(Hibernate5Module.Feature.USE_TRANSIENT_ANNOTATION);

        mapper.registerModule(module);

        messageConverter.setObjectMapper(mapper);
        return messageConverter;

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jacksonMessageConverter());
        super.configureMessageConverters(converters);
    }
}

@ControllerAdvice
public class GenericRepositoryRestExceptionHandler extends RepositoryRestExceptionHandler {

    @Autowired
    public GenericRepositoryRestExceptionHandler(MessageSource messageSource) {
        super(messageSource);
        // TODO Auto-generated constructor stub
    }

    @ResponseBody
    @ExceptionHandler(Exception.class)
    ResponseEntity<?> handleException(Exception e) {
        //  return response(HttpStatus.CONFLICT, 40902, e.getMessage());
        return response(HttpStatus.CONFLICT, 40902, e.getMessage(), e.getCause() + "", e.getClass() + "");
    }

    private ResponseEntity<RestError> response(HttpStatus status, int code, String msg) {
        return response(status, code, msg, "", "");
    }

    private ResponseEntity<RestError> response(HttpStatus status, int code, String msg, String devMsg, String moreInfo) {
        return new ResponseEntity<>(new RestError(status, code, msg, devMsg, moreInfo, null), status);
    }

}

public class CORSFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filtering on...........................................................");

        SecurityContext ctx = SecurityContextHolder.getContext();
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {}

    public void destroy() {}

}

Solution

  • Because of this

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jacksonMessageConverter());
        super.configureMessageConverters(converters);
    }
    

    Spring MVC skips all the default converters it would have otherwise registered. (If you're curious, this is done in WebMvcConfigurationSupport#getMessageConverters(..).)

    Your only HttpMessageConverter, MappingJackson2HttpMessageConverter , can only read MediaType.APPLICATION_JSON content, ie. application/json. Therefore, every other request content type will be rejected.

    You can register all the regular defaults yourself in your configureMessageConverters override (or just the ones you need to read HTML forms, XML, plain text, etc.).

    Or, you could instead override extendMessageConverters to find the default MappingJackson2HttpMessageConverter instance and configure it to use your custom ObjectMapper. For example,

    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        ObjectMapper mapper = new ObjectMapper();
        // ...initialize...
    
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter m = (MappingJackson2HttpMessageConverter) converter;
                m.setObjectMapper(mapper);
            }
        } 
    }
    

    And maybe drop a comment about relying on the default list of converters.