4

I am using Spring MVC 4.1.5 with its REST support trying to get a web service working with google protocol buffers message format. I have seen a lot of posts that are mentioning this issue with JSON format but none with google protocol buffers format. Also I don't see Spring framework docs have a MediaType for protobufs (more on that below) My code is posted here on github

Here's my controller code (using or not using produces="application-xprotobuf" makes no difference - same 406 not acceptable error)

@RestController
@RequestMapping("/ws")
@EnableWebMvc
public class CustomerRestController {
    @Autowired
    private CustomerRepository customerRepository;
    //@RequestMapping(value="/customers/{id}", method = RequestMethod.GET, produces="application/x-protobuf")
    @RequestMapping(value="/customers/{id}", method = RequestMethod.GET)
    public CustomerProtos.Customer customer(@PathVariable Integer id) {
        return this.customerRepository.findById(id);
    }

    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    private CustomerProtos.Customer customer(int id, String f, String l, Collection<String> emails) {
        String emailAddressString = "defaultEmail@address.com";
        for (String email : emails) {
            emailAddressString = email;
            break;
        }

        EmailAddress emailAddress = CustomerProtos.Customer.EmailAddress.newBuilder()
                .setType(CustomerProtos.Customer.EmailType.PROFESSIONAL).setEmail(emailAddressString).build();

        return CustomerProtos.Customer.newBuilder().setFirstName(f).setLastName(l).setId(id).addEmail(emailAddress)
                // .addAllEmail(emailAddresses)
                .build();
    }

    @Bean
    CustomerRepository customerRepository() {
        return new CustomerRepositoryImpl(); 
    }
}

here's my web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>demo</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>


<servlet>
    <servlet-name>rest</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>rest</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/rest-servlet.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

Here's my pom.xml, please note that i do have google protobuf libaries added here and they do end up in WEB-INF/lib folder of the deployed .war file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.test</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.DemoApplication</start-class>
        <java.version>1.8</java.version>
        <jackson.version>2.5.3</jackson.version>

    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.2.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.2.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.googlecode.protobuf-java-format</groupId>
            <artifactId>protobuf-java-format</artifactId>
            <version>1.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-legacy</artifactId>
            <version>1.0.0.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Here's my test client

package demo;

import java.util.Arrays;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.RestTemplate;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DemoApplicationTests {

    @Configuration
    public static class RestClientConfiguration {

        @Bean
        RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
            return new RestTemplate(Arrays.asList(hmc));
        }

        @Bean
        ProtobufHttpMessageConverter protobufHttpMessageConverter() {
            return new ProtobufHttpMessageConverter();
        }
    }

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void contextLoaded() {

        ResponseEntity<CustomerProtos.Customer> customer = restTemplate.getForEntity(
                "http://localhost:7001/demo-0.0.1-SNAPSHOT/ws/customers/2", CustomerProtos.Customer.class);

        System.out.println("customer retrieved: " + customer.toString());

    }

}

When I run the client from command line using mvn test I get the following error

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.697 sec <<< FAILURE! - in demo.DemoApplicationTests
contextLoaded(demo.DemoApplicationTests)  Time elapsed: 0.037 sec  <<< ERROR!
org.springframework.web.client.HttpClientErrorException: 406 Not Acceptable
        at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
        at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:641)
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:597)
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:557)
        at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:289)
        at demo.DemoApplicationTests.contextLoaded(DemoApplicationTests.java:42)

I saw people suggesting using a contentNegotiationManager

In my rest-servlet.xml I have mvc:annotation-driven tag

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
    <context:component-scan base-package="demo" />
    <mvc:annotation-driven />
  </beans>

Also I have dropped in a webConfig.java in demo package

package demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

  /**
    *  Total customization - see below for explanation.
    */
  @Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(false).
            favorParameter(true).
            parameterName("mediaType").
            ignoreAcceptHeader(true).
            useJaf(false).
            defaultContentType(MediaType.TEXT_PLAIN).
            mediaType("xml", MediaType.APPLICATION_XML).
            mediaType("json", MediaType.APPLICATION_JSON).
            mediaType("x-protobuf", MediaType.ALL)
            ;
  }
}

I get the 406 not acceptable error with and without the webconfig.java (which is doing the work of contentNegotationManager).

I haven't been able to find a MediaType.APPLICATION_XPROTOBUF in Spring 4.1.3 or even 4.2.3 jars .

So I have left that value to MediaType.ALL ( I think that should work ?)

When I run the test client I get this message before actually running the test..

09:27:26.198 [main] DEBUG o.s.web.client.RestTemplate - Created GET request for "http://localhost:7001/demo-0.0.1-SNAPSHOT/ws/customers/2"
09:27:26.199 [main] DEBUG o.s.web.client.RestTemplate - Setting request Accept header to [application/x-protobuf, text/plain, application/xml, application/json]
09:27:26.245 [main] DEBUG o.s.web.client.RestTemplate - GET request for "http://localhost:7001/demo-0.0.1-SNAPSHOT/ws/customers/2" resulted in 406 (Not Acceptable); invoking error
handler

It clearly shows its setting the accept headers to include "application-xprotobuf" value as well. Not sure what I am missing in this case.

Robin Bajaj
  • 2,002
  • 4
  • 29
  • 47
  • Could you try again with a proper Spring Boot application (try with start.spring.io). I can see multiple smells here that could cause many issues: 1) you don't need to add spring-mvc as a dependency yourself nor specify spring versions yourself 2) you should not have an EnableWebMvc annotation as it's done by Boot. 3) web.xml and rest-servlet.xml files should not be there 4) you should remove as much noise as possible here that does not relate to your problem – Brian Clozel Dec 17 '15 at 16:55
  • I can do those steps except that I would need to keep the web.xml and rest-servlet.xml as i am trying to get it to work on servlet 2.5 container with Java6 support. I have seen Spring boot docs mentioning i will need that web.xml if i am running in a servlet 2.5 container.. will keep you posted. thx for the response. – Robin Bajaj Dec 17 '15 at 18:47
  • 1
    The predefined MediaType constants are just a convenience feature. You can create and MediaType you want! Just create your own instance like this: new MediaType("application/x-protobuf") There are a few other constructors for subtypes, providing the charset etc. – Patrick Cornelissen Oct 08 '16 at 09:32

1 Answers1

7

I had the same issue with the controller failing with 406: Not Acceptable. After a lot of tracing I figured out the statement in the example:

Spring Boot automatically registers HttpMessageConverter beans so we need only define the ProtobufHttpMessageConverter bean and it gets configured appropriately.

is to be understood literally, since without Spring Boot no converter registration happens. The bean is instantiated, but it's never registered in the list of HttpMessageConverters.

I've solved it by explicitly registering a ProtobufHttpMessageConverter instance:

@Configuration
@EnableWebMvc
@ComponentScan
public class AppConfig extends WebMvcConfigurerAdapter {

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

You don't need to declare a separate bean for it (unless you plan to call it explicitly).

Also note that overriding configureMessageConverters() disables all standard converters; if you mean to extend the list of stock converters rather than replace it, override extendMessageConverters() instead.

yktoo
  • 2,696
  • 1
  • 23
  • 35
  • Great answer! I initially expected Spring Boot to register the converter by virtue of it existing ('cuz that's how Boot rolls... usually), but it seems like this is a necessary step. Note also that the javadocs say "as of 5.0 WebMvcConfigurer has default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for this adapter" – Jason Aug 30 '19 at 10:06